@vaultgradient/pipequery-cli 0.3.2

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.
Files changed (59) hide show
  1. package/README.md +384 -0
  2. package/dist/index.js +4630 -0
  3. package/package.json +43 -0
  4. package/src/commands/completion.ts +167 -0
  5. package/src/commands/dashboard.ts +44 -0
  6. package/src/commands/endpoint.ts +99 -0
  7. package/src/commands/init.ts +20 -0
  8. package/src/commands/monitor.ts +52 -0
  9. package/src/commands/query.ts +81 -0
  10. package/src/commands/remote.ts +89 -0
  11. package/src/commands/repl.ts +222 -0
  12. package/src/commands/serve.ts +122 -0
  13. package/src/commands/source.ts +150 -0
  14. package/src/commands/stop.ts +50 -0
  15. package/src/config/defaults.ts +48 -0
  16. package/src/config/loader.ts +37 -0
  17. package/src/config/schema.ts +73 -0
  18. package/src/docker/templates.ts +43 -0
  19. package/src/engine.ts +5 -0
  20. package/src/index.ts +164 -0
  21. package/src/server/activity-log.ts +28 -0
  22. package/src/server/app.ts +101 -0
  23. package/src/server/cache.ts +30 -0
  24. package/src/server/routes/control.ts +100 -0
  25. package/src/server/routes/dynamic.ts +51 -0
  26. package/src/server/routes/health.ts +16 -0
  27. package/src/server/routes/sse.ts +96 -0
  28. package/src/server/sources/file.ts +79 -0
  29. package/src/server/sources/manager.ts +120 -0
  30. package/src/server/sources/rest.ts +85 -0
  31. package/src/server/sources/static.ts +31 -0
  32. package/src/server/sources/types.ts +15 -0
  33. package/src/server/sources/websocket.ts +86 -0
  34. package/src/tui/App.tsx +141 -0
  35. package/src/tui/MonitorApp.tsx +271 -0
  36. package/src/tui/components/BarChart.tsx +48 -0
  37. package/src/tui/components/CandleChart.tsx +133 -0
  38. package/src/tui/components/Heatmap.tsx +92 -0
  39. package/src/tui/components/Layout.tsx +42 -0
  40. package/src/tui/components/OrderBook.tsx +94 -0
  41. package/src/tui/components/Panel.tsx +35 -0
  42. package/src/tui/components/Sparkline.tsx +55 -0
  43. package/src/tui/components/StatBox.tsx +37 -0
  44. package/src/tui/components/StatusBar.tsx +51 -0
  45. package/src/tui/components/Table.tsx +133 -0
  46. package/src/tui/hooks/useBatchedData.ts +77 -0
  47. package/src/tui/hooks/useKeyboard.ts +23 -0
  48. package/src/tui/hooks/useLiveData.ts +161 -0
  49. package/src/tui/hooks/useMouse.ts +86 -0
  50. package/src/tui/hooks/useSSE.ts +95 -0
  51. package/src/tui/hooks/useServerData.ts +37 -0
  52. package/src/utils/banner.ts +70 -0
  53. package/src/utils/logger.ts +10 -0
  54. package/src/utils/parseDuration.ts +12 -0
  55. package/src/utils/server-discovery.ts +43 -0
  56. package/src/utils/sync-output.ts +62 -0
  57. package/src/utils/yaml.ts +12 -0
  58. package/tsconfig.json +17 -0
  59. package/tsup.config.ts +13 -0
package/dist/index.js ADDED
@@ -0,0 +1,4630 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk8 from "chalk";
6
+
7
+ // src/commands/init.ts
8
+ import { writeFile } from "fs/promises";
9
+ import { resolve } from "path";
10
+ import { existsSync } from "fs";
11
+
12
+ // src/utils/logger.ts
13
+ import chalk from "chalk";
14
+ var log = {
15
+ info: (msg) => console.log(chalk.hex("#06b6d4")(" \u25CF"), msg),
16
+ success: (msg) => console.log(chalk.hex("#10b981")(" \u2713"), msg),
17
+ warn: (msg) => console.log(chalk.hex("#f59e0b")(" \u26A0"), msg),
18
+ error: (msg) => console.error(chalk.hex("#ef4444")(" \u2717"), msg),
19
+ dim: (msg) => console.log(chalk.dim(` ${msg}`)),
20
+ step: (msg) => console.log(chalk.hex("#4f46e5")(" \u2192"), msg)
21
+ };
22
+
23
+ // src/config/defaults.ts
24
+ var DEFAULT_CONFIG = {
25
+ server: {
26
+ port: 3e3,
27
+ host: "0.0.0.0"
28
+ },
29
+ sources: {},
30
+ endpoints: {},
31
+ dashboards: {}
32
+ };
33
+ var STARTER_YAML = `# PipeQuery Configuration
34
+ # Docs: https://github.com/andreadito/pipequery
35
+
36
+ server:
37
+ port: 3000
38
+ host: 0.0.0.0
39
+
40
+ # Data sources \u2014 REST APIs, WebSockets, files, or inline JSON
41
+ sources:
42
+ # Example: crypto prices from CoinCap
43
+ # crypto:
44
+ # type: rest
45
+ # url: https://api.coincap.io/v2/assets
46
+ # dataPath: data
47
+ # interval: 30s
48
+ #
49
+ # orders:
50
+ # type: file
51
+ # path: ./data/orders.csv
52
+ # watch: true
53
+
54
+ # API endpoints \u2014 expose PipeQuery results as REST APIs
55
+ endpoints: {}
56
+ # /api/top-coins:
57
+ # query: "crypto | sort(market_cap desc) | first(10)"
58
+ # cache: 30s
59
+
60
+ # Terminal dashboards
61
+ dashboards: {}
62
+ # main:
63
+ # refresh: 10s
64
+ # panels:
65
+ # - title: Top Assets
66
+ # query: "crypto | sort(market_cap desc) | first(10)"
67
+ # viz: table
68
+ `;
69
+
70
+ // src/commands/init.ts
71
+ import chalk2 from "chalk";
72
+ async function initCommand() {
73
+ const target = resolve(process.cwd(), "pipequery.yaml");
74
+ if (existsSync(target)) {
75
+ log.warn("pipequery.yaml already exists in this directory.");
76
+ return;
77
+ }
78
+ await writeFile(target, STARTER_YAML, "utf-8");
79
+ log.success("Created pipequery.yaml");
80
+ log.step(`Edit ${chalk2.white.bold("pipequery.yaml")} to configure your data sources`);
81
+ log.step(`Then run ${chalk2.white.bold("pq serve")} to start the server`);
82
+ }
83
+
84
+ // src/commands/serve.ts
85
+ import { writeFile as writeFile3, mkdir, readFile as readFile3, unlink } from "fs/promises";
86
+ import { resolve as resolve4 } from "path";
87
+ import { existsSync as existsSync3 } from "fs";
88
+ import { spawn } from "child_process";
89
+
90
+ // src/config/loader.ts
91
+ import { existsSync as existsSync2 } from "fs";
92
+ import { resolve as resolve2 } from "path";
93
+
94
+ // src/utils/yaml.ts
95
+ import { readFile, writeFile as writeFile2 } from "fs/promises";
96
+ import YAML from "yaml";
97
+ async function readYaml(path) {
98
+ const content = await readFile(path, "utf-8");
99
+ return YAML.parse(content);
100
+ }
101
+ async function writeYaml(path, data) {
102
+ const content = YAML.stringify(data, { lineWidth: 120 });
103
+ await writeFile2(path, content, "utf-8");
104
+ }
105
+
106
+ // src/config/loader.ts
107
+ var CONFIG_NAMES = ["pipequery.yaml", "pipequery.yml"];
108
+ function findConfigPath(cwd = process.cwd()) {
109
+ for (const name of CONFIG_NAMES) {
110
+ const p = resolve2(cwd, name);
111
+ if (existsSync2(p)) return p;
112
+ }
113
+ return null;
114
+ }
115
+ async function loadConfig(cwd = process.cwd()) {
116
+ const configPath = findConfigPath(cwd);
117
+ if (!configPath) {
118
+ throw new Error("No pipequery.yaml found. Run `pq init` to create one.");
119
+ }
120
+ const raw = await readYaml(configPath);
121
+ const config = {
122
+ server: { ...DEFAULT_CONFIG.server, ...raw.server },
123
+ remote: raw.remote,
124
+ sources: raw.sources ?? {},
125
+ endpoints: raw.endpoints ?? {},
126
+ dashboards: raw.dashboards ?? {}
127
+ };
128
+ return { config, path: configPath };
129
+ }
130
+ async function saveConfig(config, configPath) {
131
+ await writeYaml(configPath, config);
132
+ }
133
+
134
+ // src/server/app.ts
135
+ import Fastify from "fastify";
136
+ import cors from "@fastify/cors";
137
+
138
+ // src/utils/parseDuration.ts
139
+ var UNITS = {
140
+ ms: 1,
141
+ s: 1e3,
142
+ m: 6e4,
143
+ h: 36e5
144
+ };
145
+ function parseDuration(input) {
146
+ const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/);
147
+ if (!match) throw new Error(`Invalid duration: "${input}". Use format like "30s", "5m", "1h".`);
148
+ return parseFloat(match[1]) * UNITS[match[2]];
149
+ }
150
+
151
+ // src/server/sources/rest.ts
152
+ function extractPath(obj, path) {
153
+ let current = obj;
154
+ for (const key of path.split(".")) {
155
+ if (current == null || typeof current !== "object") return [];
156
+ current = current[key];
157
+ }
158
+ return Array.isArray(current) ? current : [];
159
+ }
160
+ var RestSourceAdapter = class {
161
+ constructor(config) {
162
+ this.config = config;
163
+ }
164
+ config;
165
+ data = [];
166
+ timer = null;
167
+ lastFetch = null;
168
+ error;
169
+ listeners = /* @__PURE__ */ new Set();
170
+ async start() {
171
+ await this.fetch();
172
+ const interval = this.config.interval ? parseDuration(this.config.interval) : 3e4;
173
+ this.timer = setInterval(() => this.fetch(), interval);
174
+ }
175
+ stop() {
176
+ if (this.timer) {
177
+ clearInterval(this.timer);
178
+ this.timer = null;
179
+ }
180
+ }
181
+ getData() {
182
+ return this.data;
183
+ }
184
+ getStatus() {
185
+ return {
186
+ healthy: !this.error,
187
+ rowCount: this.data.length,
188
+ lastFetch: this.lastFetch,
189
+ error: this.error
190
+ };
191
+ }
192
+ onUpdate(callback) {
193
+ this.listeners.add(callback);
194
+ return () => this.listeners.delete(callback);
195
+ }
196
+ async refresh() {
197
+ await this.fetch();
198
+ }
199
+ async fetch() {
200
+ try {
201
+ let url = this.config.url;
202
+ if (this.config.params) {
203
+ const params = new URLSearchParams(this.config.params);
204
+ url += (url.includes("?") ? "&" : "?") + params.toString();
205
+ }
206
+ const res = await globalThis.fetch(url, {
207
+ headers: this.config.headers
208
+ });
209
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
210
+ const json = await res.json();
211
+ this.data = this.config.dataPath ? extractPath(json, this.config.dataPath) : Array.isArray(json) ? json : [json];
212
+ this.lastFetch = /* @__PURE__ */ new Date();
213
+ this.error = void 0;
214
+ this.notify();
215
+ } catch (err) {
216
+ this.error = err instanceof Error ? err.message : String(err);
217
+ }
218
+ }
219
+ notify() {
220
+ for (const cb of this.listeners) cb(this.data);
221
+ }
222
+ };
223
+
224
+ // src/server/sources/websocket.ts
225
+ import WebSocket from "ws";
226
+ var WebSocketSourceAdapter = class {
227
+ constructor(config) {
228
+ this.config = config;
229
+ this.maxBuffer = config.maxBuffer ?? 1e3;
230
+ }
231
+ config;
232
+ data = [];
233
+ ws = null;
234
+ lastFetch = null;
235
+ error;
236
+ listeners = /* @__PURE__ */ new Set();
237
+ maxBuffer;
238
+ async start() {
239
+ return new Promise((resolve9, reject) => {
240
+ this.ws = new WebSocket(this.config.url);
241
+ this.ws.on("open", () => {
242
+ this.error = void 0;
243
+ resolve9();
244
+ });
245
+ this.ws.on("message", (raw) => {
246
+ try {
247
+ const parsed = JSON.parse(raw.toString());
248
+ if (Array.isArray(parsed)) {
249
+ this.data = parsed;
250
+ } else {
251
+ this.data.push(parsed);
252
+ if (this.data.length > this.maxBuffer) {
253
+ this.data = this.data.slice(-this.maxBuffer);
254
+ }
255
+ }
256
+ this.lastFetch = /* @__PURE__ */ new Date();
257
+ this.error = void 0;
258
+ this.notify();
259
+ } catch {
260
+ }
261
+ });
262
+ this.ws.on("error", (err) => {
263
+ this.error = err.message;
264
+ reject(err);
265
+ });
266
+ this.ws.on("close", () => {
267
+ setTimeout(() => {
268
+ if (this.ws) this.start().catch(() => {
269
+ });
270
+ }, 5e3);
271
+ });
272
+ });
273
+ }
274
+ stop() {
275
+ const ws = this.ws;
276
+ this.ws = null;
277
+ ws?.close();
278
+ }
279
+ getData() {
280
+ return this.data;
281
+ }
282
+ getStatus() {
283
+ return {
284
+ healthy: this.ws?.readyState === WebSocket.OPEN,
285
+ rowCount: this.data.length,
286
+ lastFetch: this.lastFetch,
287
+ error: this.error
288
+ };
289
+ }
290
+ onUpdate(callback) {
291
+ this.listeners.add(callback);
292
+ return () => this.listeners.delete(callback);
293
+ }
294
+ notify() {
295
+ for (const cb of this.listeners) cb(this.data);
296
+ }
297
+ };
298
+
299
+ // src/server/sources/file.ts
300
+ import { readFile as readFile2 } from "fs/promises";
301
+ import { extname, resolve as resolve3 } from "path";
302
+ import { watch } from "chokidar";
303
+ import { parse as csvParse } from "csv-parse/sync";
304
+ var FileSourceAdapter = class {
305
+ constructor(config, cwd) {
306
+ this.config = config;
307
+ this.cwd = cwd;
308
+ this.resolvedPath = resolve3(cwd, config.path);
309
+ }
310
+ config;
311
+ cwd;
312
+ data = [];
313
+ watcher = null;
314
+ lastFetch = null;
315
+ error;
316
+ listeners = /* @__PURE__ */ new Set();
317
+ resolvedPath;
318
+ async start() {
319
+ await this.load();
320
+ if (this.config.watch) {
321
+ this.watcher = watch(this.resolvedPath, { ignoreInitial: true });
322
+ this.watcher.on("change", () => this.load());
323
+ }
324
+ }
325
+ stop() {
326
+ this.watcher?.close();
327
+ this.watcher = null;
328
+ }
329
+ getData() {
330
+ return this.data;
331
+ }
332
+ getStatus() {
333
+ return {
334
+ healthy: !this.error,
335
+ rowCount: this.data.length,
336
+ lastFetch: this.lastFetch,
337
+ error: this.error
338
+ };
339
+ }
340
+ onUpdate(callback) {
341
+ this.listeners.add(callback);
342
+ return () => this.listeners.delete(callback);
343
+ }
344
+ async load() {
345
+ try {
346
+ const content = await readFile2(this.resolvedPath, "utf-8");
347
+ const format = this.config.format ?? this.detectFormat();
348
+ if (format === "csv") {
349
+ this.data = csvParse(content, { columns: true, skip_empty_lines: true, cast: true });
350
+ } else {
351
+ const parsed = JSON.parse(content);
352
+ this.data = Array.isArray(parsed) ? parsed : [parsed];
353
+ }
354
+ this.lastFetch = /* @__PURE__ */ new Date();
355
+ this.error = void 0;
356
+ this.notify();
357
+ } catch (err) {
358
+ this.error = err instanceof Error ? err.message : String(err);
359
+ }
360
+ }
361
+ detectFormat() {
362
+ const ext = extname(this.resolvedPath).toLowerCase();
363
+ return ext === ".csv" || ext === ".tsv" ? "csv" : "json";
364
+ }
365
+ notify() {
366
+ for (const cb of this.listeners) cb(this.data);
367
+ }
368
+ };
369
+
370
+ // src/server/sources/static.ts
371
+ var StaticSourceAdapter = class {
372
+ constructor(config) {
373
+ this.config = config;
374
+ }
375
+ config;
376
+ listeners = /* @__PURE__ */ new Set();
377
+ async start() {
378
+ }
379
+ stop() {
380
+ }
381
+ getData() {
382
+ return this.config.data;
383
+ }
384
+ getStatus() {
385
+ return {
386
+ healthy: true,
387
+ rowCount: this.config.data.length,
388
+ lastFetch: /* @__PURE__ */ new Date()
389
+ };
390
+ }
391
+ onUpdate(callback) {
392
+ this.listeners.add(callback);
393
+ return () => this.listeners.delete(callback);
394
+ }
395
+ };
396
+
397
+ // src/server/sources/manager.ts
398
+ var SourceManager = class {
399
+ constructor(cwd) {
400
+ this.cwd = cwd;
401
+ }
402
+ cwd;
403
+ sources = /* @__PURE__ */ new Map();
404
+ globalListeners = /* @__PURE__ */ new Set();
405
+ async addSource(name, config) {
406
+ if (this.sources.has(name)) {
407
+ this.removeSource(name);
408
+ }
409
+ const adapter = this.createAdapter(config);
410
+ const entry = { config, adapter };
411
+ entry.unsubscribe = adapter.onUpdate((data) => {
412
+ for (const listener of this.globalListeners) {
413
+ listener(name, data);
414
+ }
415
+ });
416
+ this.sources.set(name, entry);
417
+ await adapter.start();
418
+ }
419
+ removeSource(name) {
420
+ const entry = this.sources.get(name);
421
+ if (!entry) return false;
422
+ entry.unsubscribe?.();
423
+ entry.adapter.stop();
424
+ this.sources.delete(name);
425
+ return true;
426
+ }
427
+ getContext() {
428
+ const ctx = {};
429
+ for (const [name, { adapter }] of this.sources) {
430
+ ctx[name] = adapter.getData();
431
+ }
432
+ return ctx;
433
+ }
434
+ getSourceData(name) {
435
+ return this.sources.get(name)?.adapter.getData();
436
+ }
437
+ getSourceStatus(name) {
438
+ return this.sources.get(name)?.adapter.getStatus();
439
+ }
440
+ getAllStatuses() {
441
+ const result = {};
442
+ for (const [name, { adapter }] of this.sources) {
443
+ result[name] = adapter.getStatus();
444
+ }
445
+ return result;
446
+ }
447
+ getSourceNames() {
448
+ return [...this.sources.keys()];
449
+ }
450
+ /**
451
+ * Subscribe to data updates from any source.
452
+ * Used by SSE endpoint for real-time push to TUI clients.
453
+ * Returns unsubscribe function.
454
+ */
455
+ onSourceUpdate(callback) {
456
+ this.globalListeners.add(callback);
457
+ return () => this.globalListeners.delete(callback);
458
+ }
459
+ async refreshAll() {
460
+ await Promise.all(
461
+ [...this.sources.values()].map(({ adapter }) => adapter.refresh?.() ?? Promise.resolve())
462
+ );
463
+ }
464
+ async dispose() {
465
+ this.globalListeners.clear();
466
+ for (const entry of this.sources.values()) {
467
+ entry.unsubscribe?.();
468
+ entry.adapter.stop();
469
+ }
470
+ this.sources.clear();
471
+ }
472
+ createAdapter(config) {
473
+ switch (config.type) {
474
+ case "rest":
475
+ return new RestSourceAdapter(config);
476
+ case "websocket":
477
+ return new WebSocketSourceAdapter(config);
478
+ case "file":
479
+ return new FileSourceAdapter(config, this.cwd);
480
+ case "static":
481
+ return new StaticSourceAdapter(config);
482
+ default:
483
+ throw new Error(`Unknown source type: ${config.type}`);
484
+ }
485
+ }
486
+ };
487
+
488
+ // src/server/routes/health.ts
489
+ function registerHealthRoutes(app, sourceManager, startedAt) {
490
+ app.get("/health", async () => {
491
+ return { status: "ok", uptime: Date.now() - startedAt.getTime() };
492
+ });
493
+ app.get("/status", async () => {
494
+ return {
495
+ status: "ok",
496
+ uptime: Date.now() - startedAt.getTime(),
497
+ sources: sourceManager.getAllStatuses()
498
+ };
499
+ });
500
+ }
501
+
502
+ // ../src/engine/types.ts
503
+ var AGGREGATE_NAMES = /* @__PURE__ */ new Set([
504
+ "sum",
505
+ "avg",
506
+ "min",
507
+ "max",
508
+ "count",
509
+ "median",
510
+ "stddev",
511
+ "var",
512
+ "percentile",
513
+ "skew",
514
+ "kurt",
515
+ "vwap",
516
+ "wavg",
517
+ "drawdown",
518
+ "pct",
519
+ "sharpe",
520
+ "calmar",
521
+ "sortino",
522
+ "info_ratio",
523
+ "distinct_count",
524
+ "sum_abs",
525
+ "abs_sum",
526
+ "first_value",
527
+ "last_value"
528
+ ]);
529
+ var TWO_ARG_AGGREGATES = /* @__PURE__ */ new Set([
530
+ "percentile",
531
+ "vwap",
532
+ "wavg",
533
+ "info_ratio"
534
+ ]);
535
+ var DataWeaveError = class extends Error {
536
+ position;
537
+ line;
538
+ column;
539
+ constructor(message, position, line, column) {
540
+ super(message);
541
+ this.name = "DataWeaveError";
542
+ this.position = position;
543
+ this.line = line;
544
+ this.column = column;
545
+ }
546
+ };
547
+ var LexerError = class extends DataWeaveError {
548
+ constructor(message, position, line, column) {
549
+ super(message, position, line, column);
550
+ this.name = "LexerError";
551
+ }
552
+ };
553
+ var ParseError = class extends DataWeaveError {
554
+ constructor(message, position, line, column) {
555
+ super(message, position, line, column);
556
+ this.name = "ParseError";
557
+ }
558
+ };
559
+ var RuntimeError = class extends Error {
560
+ constructor(message) {
561
+ super(message);
562
+ this.name = "RuntimeError";
563
+ }
564
+ };
565
+
566
+ // ../src/engine/lexer.ts
567
+ var KEYWORDS = {
568
+ as: "AS",
569
+ asc: "ASC",
570
+ desc: "DESC",
571
+ true: "BOOLEAN",
572
+ false: "BOOLEAN",
573
+ null: "NULL"
574
+ };
575
+ function tokenize(source2) {
576
+ const tokens = [];
577
+ let pos = 0;
578
+ let line = 1;
579
+ let column = 1;
580
+ function advance() {
581
+ const ch = source2[pos];
582
+ pos++;
583
+ if (ch === "\n") {
584
+ line++;
585
+ column = 1;
586
+ } else {
587
+ column++;
588
+ }
589
+ return ch;
590
+ }
591
+ function peek() {
592
+ return source2[pos];
593
+ }
594
+ function peekNext() {
595
+ return source2[pos + 1];
596
+ }
597
+ function addToken(type, value, startPos, startLine, startCol) {
598
+ tokens.push({ type, value, position: startPos, line: startLine, column: startCol });
599
+ }
600
+ function error2(msg) {
601
+ throw new LexerError(msg, pos, line, column);
602
+ }
603
+ while (pos < source2.length) {
604
+ const startPos = pos;
605
+ const startLine = line;
606
+ const startCol = column;
607
+ const ch = peek();
608
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
609
+ advance();
610
+ continue;
611
+ }
612
+ if (ch === "|") {
613
+ advance();
614
+ addToken("PIPE", "|", startPos, startLine, startCol);
615
+ continue;
616
+ }
617
+ if (ch === "(") {
618
+ advance();
619
+ addToken("LPAREN", "(", startPos, startLine, startCol);
620
+ continue;
621
+ }
622
+ if (ch === ")") {
623
+ advance();
624
+ addToken("RPAREN", ")", startPos, startLine, startCol);
625
+ continue;
626
+ }
627
+ if (ch === ",") {
628
+ advance();
629
+ addToken("COMMA", ",", startPos, startLine, startCol);
630
+ continue;
631
+ }
632
+ if (ch === ".") {
633
+ advance();
634
+ addToken("DOT", ".", startPos, startLine, startCol);
635
+ continue;
636
+ }
637
+ if (ch === "+") {
638
+ advance();
639
+ addToken("PLUS", "+", startPos, startLine, startCol);
640
+ continue;
641
+ }
642
+ if (ch === "-") {
643
+ advance();
644
+ addToken("MINUS", "-", startPos, startLine, startCol);
645
+ continue;
646
+ }
647
+ if (ch === "*") {
648
+ advance();
649
+ addToken("STAR", "*", startPos, startLine, startCol);
650
+ continue;
651
+ }
652
+ if (ch === "/") {
653
+ advance();
654
+ addToken("SLASH", "/", startPos, startLine, startCol);
655
+ continue;
656
+ }
657
+ if (ch === "%") {
658
+ advance();
659
+ addToken("MOD", "%", startPos, startLine, startCol);
660
+ continue;
661
+ }
662
+ if (ch === ">" && peekNext() === "=") {
663
+ advance();
664
+ advance();
665
+ addToken("GTE", ">=", startPos, startLine, startCol);
666
+ continue;
667
+ }
668
+ if (ch === ">") {
669
+ advance();
670
+ addToken("GT", ">", startPos, startLine, startCol);
671
+ continue;
672
+ }
673
+ if (ch === "<" && peekNext() === "=") {
674
+ advance();
675
+ advance();
676
+ addToken("LTE", "<=", startPos, startLine, startCol);
677
+ continue;
678
+ }
679
+ if (ch === "<") {
680
+ advance();
681
+ addToken("LT", "<", startPos, startLine, startCol);
682
+ continue;
683
+ }
684
+ if (ch === "=" && peekNext() === "=") {
685
+ advance();
686
+ advance();
687
+ addToken("EQ", "==", startPos, startLine, startCol);
688
+ continue;
689
+ }
690
+ if (ch === "!" && peekNext() === "=") {
691
+ advance();
692
+ advance();
693
+ addToken("NEQ", "!=", startPos, startLine, startCol);
694
+ continue;
695
+ }
696
+ if (ch === "!") {
697
+ advance();
698
+ addToken("NOT", "!", startPos, startLine, startCol);
699
+ continue;
700
+ }
701
+ if (ch === "&" && peekNext() === "&") {
702
+ advance();
703
+ advance();
704
+ addToken("AND", "&&", startPos, startLine, startCol);
705
+ continue;
706
+ }
707
+ if (ch === "|" && peekNext() === "|") {
708
+ advance();
709
+ advance();
710
+ addToken("OR", "||", startPos, startLine, startCol);
711
+ continue;
712
+ }
713
+ if (ch >= "0" && ch <= "9") {
714
+ const start = pos;
715
+ while (pos < source2.length && peek() >= "0" && peek() <= "9") advance();
716
+ if (pos < source2.length && peek() === "." && pos + 1 < source2.length && source2[pos + 1] >= "0" && source2[pos + 1] <= "9") {
717
+ advance();
718
+ while (pos < source2.length && peek() >= "0" && peek() <= "9") advance();
719
+ }
720
+ addToken("NUMBER", source2.slice(start, pos), startPos, startLine, startCol);
721
+ continue;
722
+ }
723
+ if (ch === '"' || ch === "'") {
724
+ const quote = ch;
725
+ advance();
726
+ const start = pos;
727
+ let value = "";
728
+ while (pos < source2.length && peek() !== quote) {
729
+ if (peek() === "\\" && pos + 1 < source2.length) {
730
+ advance();
731
+ const escaped = advance();
732
+ if (escaped === "n") value += "\n";
733
+ else if (escaped === "t") value += " ";
734
+ else value += escaped;
735
+ } else {
736
+ value += advance();
737
+ }
738
+ }
739
+ if (pos >= source2.length) {
740
+ error2(`Unterminated string starting at line ${startLine}, column ${startCol}`);
741
+ }
742
+ advance();
743
+ void start;
744
+ addToken("STRING", value, startPos, startLine, startCol);
745
+ continue;
746
+ }
747
+ if (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_") {
748
+ const start = pos;
749
+ while (pos < source2.length && (peek() >= "a" && peek() <= "z" || peek() >= "A" && peek() <= "Z" || peek() >= "0" && peek() <= "9" || peek() === "_")) {
750
+ advance();
751
+ }
752
+ const word = source2.slice(start, pos);
753
+ const keywordType = KEYWORDS[word];
754
+ if (keywordType) {
755
+ addToken(keywordType, word, startPos, startLine, startCol);
756
+ } else {
757
+ addToken("IDENTIFIER", word, startPos, startLine, startCol);
758
+ }
759
+ continue;
760
+ }
761
+ error2(`Unexpected character '${ch}'`);
762
+ }
763
+ addToken("EOF", "", pos, line, column);
764
+ return tokens;
765
+ }
766
+
767
+ // ../src/engine/parser.ts
768
+ function parse(tokens) {
769
+ let pos = 0;
770
+ function peek() {
771
+ return tokens[pos];
772
+ }
773
+ function advance() {
774
+ const token = tokens[pos];
775
+ pos++;
776
+ return token;
777
+ }
778
+ function expect(type, context) {
779
+ const token = peek();
780
+ if (token.type !== type) {
781
+ const ctx = context ? ` ${context}` : "";
782
+ throw new ParseError(
783
+ `Expected ${type} but found ${token.type} ('${token.value}')${ctx}`,
784
+ token.position,
785
+ token.line,
786
+ token.column
787
+ );
788
+ }
789
+ return advance();
790
+ }
791
+ function match(type) {
792
+ if (peek().type === type) {
793
+ advance();
794
+ return true;
795
+ }
796
+ return false;
797
+ }
798
+ function parseExpression() {
799
+ let expr = parseOrExpr();
800
+ if (peek().type === "AS") {
801
+ advance();
802
+ const nameToken = expect("IDENTIFIER", 'after "as"');
803
+ expr = { kind: "AliasExpr", expression: expr, alias: nameToken.value };
804
+ }
805
+ return expr;
806
+ }
807
+ function parseOrExpr() {
808
+ let left = parseAndExpr();
809
+ while (peek().type === "OR") {
810
+ advance();
811
+ const right = parseAndExpr();
812
+ left = { kind: "BinaryExpr", operator: "||", left, right };
813
+ }
814
+ return left;
815
+ }
816
+ function parseAndExpr() {
817
+ let left = parseEqualityExpr();
818
+ while (peek().type === "AND") {
819
+ advance();
820
+ const right = parseEqualityExpr();
821
+ left = { kind: "BinaryExpr", operator: "&&", left, right };
822
+ }
823
+ return left;
824
+ }
825
+ function parseEqualityExpr() {
826
+ let left = parseComparisonExpr();
827
+ while (peek().type === "EQ" || peek().type === "NEQ") {
828
+ const op = advance().value;
829
+ const right = parseComparisonExpr();
830
+ left = { kind: "BinaryExpr", operator: op, left, right };
831
+ }
832
+ return left;
833
+ }
834
+ function parseComparisonExpr() {
835
+ let left = parseAdditiveExpr();
836
+ while (peek().type === "GT" || peek().type === "GTE" || peek().type === "LT" || peek().type === "LTE") {
837
+ const op = advance().value;
838
+ const right = parseAdditiveExpr();
839
+ left = { kind: "BinaryExpr", operator: op, left, right };
840
+ }
841
+ return left;
842
+ }
843
+ function parseAdditiveExpr() {
844
+ let left = parseMultiplicativeExpr();
845
+ while (peek().type === "PLUS" || peek().type === "MINUS") {
846
+ const op = advance().value;
847
+ const right = parseMultiplicativeExpr();
848
+ left = { kind: "BinaryExpr", operator: op, left, right };
849
+ }
850
+ return left;
851
+ }
852
+ function parseMultiplicativeExpr() {
853
+ let left = parseUnaryExpr();
854
+ while (peek().type === "STAR" || peek().type === "SLASH" || peek().type === "MOD") {
855
+ const op = advance().value;
856
+ const right = parseUnaryExpr();
857
+ left = { kind: "BinaryExpr", operator: op, left, right };
858
+ }
859
+ return left;
860
+ }
861
+ function parseUnaryExpr() {
862
+ if (peek().type === "NOT") {
863
+ advance();
864
+ const operand = parseUnaryExpr();
865
+ return { kind: "UnaryExpr", operator: "!", operand };
866
+ }
867
+ if (peek().type === "MINUS") {
868
+ advance();
869
+ const operand = parseUnaryExpr();
870
+ return { kind: "UnaryExpr", operator: "-", operand };
871
+ }
872
+ return parsePrimaryExpr();
873
+ }
874
+ function parsePrimaryExpr() {
875
+ const token = peek();
876
+ if (token.type === "NUMBER") {
877
+ advance();
878
+ return { kind: "NumberLiteral", value: Number(token.value) };
879
+ }
880
+ if (token.type === "STRING") {
881
+ advance();
882
+ return { kind: "StringLiteral", value: token.value };
883
+ }
884
+ if (token.type === "BOOLEAN") {
885
+ advance();
886
+ return { kind: "BooleanLiteral", value: token.value === "true" };
887
+ }
888
+ if (token.type === "NULL") {
889
+ advance();
890
+ return { kind: "NullLiteral" };
891
+ }
892
+ if (token.type === "IDENTIFIER") {
893
+ advance();
894
+ const name = token.value;
895
+ if (peek().type === "LPAREN") {
896
+ advance();
897
+ const args = [];
898
+ if (peek().type !== "RPAREN") {
899
+ args.push(parseExpression());
900
+ while (match("COMMA")) {
901
+ args.push(parseExpression());
902
+ }
903
+ }
904
+ expect("RPAREN", "after function arguments");
905
+ return { kind: "FunctionCall", name, args };
906
+ }
907
+ const path = [name];
908
+ while (peek().type === "DOT") {
909
+ advance();
910
+ const next = expect("IDENTIFIER", 'after "."');
911
+ path.push(next.value);
912
+ }
913
+ return { kind: "FieldAccess", path };
914
+ }
915
+ if (token.type === "LPAREN") {
916
+ advance();
917
+ const expr = parseExpression();
918
+ expect("RPAREN", "after grouped expression");
919
+ return expr;
920
+ }
921
+ throw new ParseError(
922
+ `Unexpected token ${token.type} ('${token.value}')`,
923
+ token.position,
924
+ token.line,
925
+ token.column
926
+ );
927
+ }
928
+ function isAggregateExpression(expr) {
929
+ if (expr.kind === "AliasExpr") return isAggregateExpression(expr.expression);
930
+ if (expr.kind === "FunctionCall") return AGGREGATE_NAMES.has(expr.name);
931
+ return false;
932
+ }
933
+ function parseOperation() {
934
+ const nameToken = expect("IDENTIFIER", 'after "|"');
935
+ const name = nameToken.value;
936
+ expect("LPAREN", `after "${name}"`);
937
+ let op;
938
+ switch (name) {
939
+ case "where": {
940
+ const condition = parseExpression();
941
+ op = { kind: "WhereOp", condition };
942
+ break;
943
+ }
944
+ case "select": {
945
+ const fields = [];
946
+ if (peek().type !== "RPAREN") {
947
+ fields.push(parseExpression());
948
+ while (match("COMMA")) {
949
+ fields.push(parseExpression());
950
+ }
951
+ }
952
+ op = { kind: "SelectOp", fields };
953
+ break;
954
+ }
955
+ case "sort": {
956
+ const criteria = [];
957
+ if (peek().type !== "RPAREN") {
958
+ const expr = parseOrExpr();
959
+ let direction = "asc";
960
+ if (peek().type === "ASC") {
961
+ advance();
962
+ direction = "asc";
963
+ } else if (peek().type === "DESC") {
964
+ advance();
965
+ direction = "desc";
966
+ }
967
+ criteria.push({ expression: expr, direction });
968
+ while (match("COMMA")) {
969
+ const e = parseOrExpr();
970
+ let d = "asc";
971
+ if (peek().type === "ASC") {
972
+ advance();
973
+ d = "asc";
974
+ } else if (peek().type === "DESC") {
975
+ advance();
976
+ d = "desc";
977
+ }
978
+ criteria.push({ expression: e, direction: d });
979
+ }
980
+ }
981
+ op = { kind: "SortOp", criteria };
982
+ break;
983
+ }
984
+ case "groupBy": {
985
+ const keys = [];
986
+ if (peek().type !== "RPAREN") {
987
+ keys.push(parseOrExpr());
988
+ while (match("COMMA")) {
989
+ keys.push(parseOrExpr());
990
+ }
991
+ }
992
+ op = { kind: "GroupByOp", keys };
993
+ break;
994
+ }
995
+ case "join": {
996
+ const rightToken = expect("IDENTIFIER", "for join source");
997
+ const right = rightToken.value;
998
+ expect("COMMA", "after join source");
999
+ const condition = parseExpression();
1000
+ op = { kind: "JoinOp", right, condition };
1001
+ break;
1002
+ }
1003
+ case "first": {
1004
+ const numToken = expect("NUMBER", "for first count");
1005
+ op = { kind: "FirstOp", count: Number(numToken.value) };
1006
+ break;
1007
+ }
1008
+ case "last": {
1009
+ const numToken = expect("NUMBER", "for last count");
1010
+ op = { kind: "LastOp", count: Number(numToken.value) };
1011
+ break;
1012
+ }
1013
+ case "distinct": {
1014
+ const fields = [];
1015
+ if (peek().type !== "RPAREN") {
1016
+ fields.push(parseOrExpr());
1017
+ while (match("COMMA")) {
1018
+ fields.push(parseOrExpr());
1019
+ }
1020
+ }
1021
+ op = { kind: "DistinctOp", fields: fields.length > 0 ? fields : void 0 };
1022
+ break;
1023
+ }
1024
+ case "flatten": {
1025
+ let field;
1026
+ if (peek().type !== "RPAREN") {
1027
+ field = parseOrExpr();
1028
+ }
1029
+ op = { kind: "FlattenOp", field };
1030
+ break;
1031
+ }
1032
+ case "transpose": {
1033
+ let headerField;
1034
+ if (peek().type !== "RPAREN") {
1035
+ headerField = parseOrExpr();
1036
+ }
1037
+ op = { kind: "TransposeOp", headerField };
1038
+ break;
1039
+ }
1040
+ case "rollup": {
1041
+ const keys = [];
1042
+ const aggregates = [];
1043
+ let seenAggregate = false;
1044
+ if (peek().type !== "RPAREN") {
1045
+ const expr = parseExpression();
1046
+ if (isAggregateExpression(expr)) {
1047
+ seenAggregate = true;
1048
+ aggregates.push(expr);
1049
+ } else {
1050
+ keys.push(expr);
1051
+ }
1052
+ while (match("COMMA")) {
1053
+ const e = parseExpression();
1054
+ if (isAggregateExpression(e)) {
1055
+ seenAggregate = true;
1056
+ aggregates.push(e);
1057
+ } else if (seenAggregate) {
1058
+ const t = peek();
1059
+ throw new ParseError(
1060
+ "rollup: group keys must come before aggregate expressions",
1061
+ t.position,
1062
+ t.line,
1063
+ t.column
1064
+ );
1065
+ } else {
1066
+ keys.push(e);
1067
+ }
1068
+ }
1069
+ }
1070
+ if (aggregates.length === 0) {
1071
+ const t = peek();
1072
+ throw new ParseError(
1073
+ "rollup: at least one aggregate expression is required",
1074
+ t.position,
1075
+ t.line,
1076
+ t.column
1077
+ );
1078
+ }
1079
+ op = { kind: "RollupOp", keys, aggregates };
1080
+ break;
1081
+ }
1082
+ case "pivot": {
1083
+ const pivotField = parseOrExpr();
1084
+ expect("COMMA", "after pivot field");
1085
+ const aggregates = [];
1086
+ aggregates.push(parseExpression());
1087
+ while (match("COMMA")) {
1088
+ aggregates.push(parseExpression());
1089
+ }
1090
+ op = { kind: "PivotOp", pivotField, aggregates };
1091
+ break;
1092
+ }
1093
+ case "map": {
1094
+ const fields = [];
1095
+ if (peek().type !== "RPAREN") {
1096
+ fields.push(parseExpression());
1097
+ while (match("COMMA")) {
1098
+ fields.push(parseExpression());
1099
+ }
1100
+ }
1101
+ op = { kind: "MapOp", fields };
1102
+ break;
1103
+ }
1104
+ case "reduce": {
1105
+ const initial = parseExpression();
1106
+ expect("COMMA", "after reduce initial value");
1107
+ const accumulator = parseExpression();
1108
+ op = { kind: "ReduceOp", initial, accumulator };
1109
+ break;
1110
+ }
1111
+ default: {
1112
+ if (AGGREGATE_NAMES.has(name)) {
1113
+ if (TWO_ARG_AGGREGATES.has(name)) {
1114
+ const args = [];
1115
+ args.push(parseOrExpr());
1116
+ expect("COMMA", `after first argument of ${name}`);
1117
+ args.push(parseOrExpr());
1118
+ op = { kind: "AggregateOp", function: name, args };
1119
+ } else {
1120
+ let field;
1121
+ if (peek().type !== "RPAREN") {
1122
+ field = parseOrExpr();
1123
+ }
1124
+ op = { kind: "AggregateOp", function: name, field };
1125
+ }
1126
+ break;
1127
+ }
1128
+ throw new ParseError(
1129
+ `Unknown operation "${name}"`,
1130
+ nameToken.position,
1131
+ nameToken.line,
1132
+ nameToken.column
1133
+ );
1134
+ }
1135
+ }
1136
+ expect("RPAREN", `after ${name} arguments`);
1137
+ return op;
1138
+ }
1139
+ function parsePipeline() {
1140
+ const sourceToken = expect("IDENTIFIER", "at start of pipeline");
1141
+ const operations = [];
1142
+ while (peek().type === "PIPE") {
1143
+ advance();
1144
+ operations.push(parseOperation());
1145
+ }
1146
+ if (peek().type !== "EOF") {
1147
+ const t = peek();
1148
+ throw new ParseError(
1149
+ `Unexpected token ${t.type} ('${t.value}') after pipeline`,
1150
+ t.position,
1151
+ t.line,
1152
+ t.column
1153
+ );
1154
+ }
1155
+ return { kind: "Pipeline", source: sourceToken.value, operations };
1156
+ }
1157
+ return parsePipeline();
1158
+ }
1159
+
1160
+ // ../src/engine/runtime.ts
1161
+ var accessorCache = /* @__PURE__ */ new Map();
1162
+ function getFieldAccessor(path) {
1163
+ const key = path.join(".");
1164
+ let fn = accessorCache.get(key);
1165
+ if (fn) return fn;
1166
+ if (path.length === 1) {
1167
+ const field = path[0];
1168
+ fn = (obj) => obj[field];
1169
+ } else {
1170
+ const segments = [...path];
1171
+ fn = (obj) => {
1172
+ let val = obj;
1173
+ for (const segment of segments) {
1174
+ if (val == null) return void 0;
1175
+ val = val[segment];
1176
+ }
1177
+ return val;
1178
+ };
1179
+ }
1180
+ accessorCache.set(key, fn);
1181
+ return fn;
1182
+ }
1183
+ function compareValues(a, b) {
1184
+ if (a === b) return 0;
1185
+ if (a == null && b == null) return 0;
1186
+ if (a == null) return -1;
1187
+ if (b == null) return 1;
1188
+ if (typeof a === "number" && typeof b === "number") return a - b;
1189
+ return String(a).localeCompare(String(b));
1190
+ }
1191
+ function groupByFn(data, keyFns) {
1192
+ const groups = /* @__PURE__ */ new Map();
1193
+ if (keyFns.length === 1) {
1194
+ const { fn, name } = keyFns[0];
1195
+ for (const row of data) {
1196
+ const val = fn(row);
1197
+ const groupKey = String(val);
1198
+ let group = groups.get(groupKey);
1199
+ if (!group) {
1200
+ group = { keys: { [name]: val }, rows: [] };
1201
+ groups.set(groupKey, group);
1202
+ }
1203
+ group.rows.push(row);
1204
+ }
1205
+ } else {
1206
+ for (const row of data) {
1207
+ const keys = {};
1208
+ let groupKey = "";
1209
+ for (let i = 0; i < keyFns.length; i++) {
1210
+ const { fn, name } = keyFns[i];
1211
+ const val = fn(row);
1212
+ keys[name] = val;
1213
+ groupKey += (i > 0 ? "\0" : "") + String(val);
1214
+ }
1215
+ let group = groups.get(groupKey);
1216
+ if (!group) {
1217
+ group = { keys, rows: [] };
1218
+ groups.set(groupKey, group);
1219
+ }
1220
+ group.rows.push(row);
1221
+ }
1222
+ }
1223
+ return Array.from(groups.values());
1224
+ }
1225
+ function nestedLoopJoin(left, right, conditionFn) {
1226
+ const result = [];
1227
+ for (const l of left) {
1228
+ for (const r of right) {
1229
+ const combined = { ...l, ...r };
1230
+ if (conditionFn(combined)) {
1231
+ result.push(combined);
1232
+ }
1233
+ }
1234
+ }
1235
+ return result;
1236
+ }
1237
+ function hashJoin(left, right, leftKeyFn, rightKeyFn) {
1238
+ const hashMap = /* @__PURE__ */ new Map();
1239
+ for (const r of right) {
1240
+ const key = rightKeyFn(r);
1241
+ let bucket = hashMap.get(key);
1242
+ if (!bucket) {
1243
+ bucket = [];
1244
+ hashMap.set(key, bucket);
1245
+ }
1246
+ bucket.push(r);
1247
+ }
1248
+ const result = [];
1249
+ for (const l of left) {
1250
+ const matches = hashMap.get(leftKeyFn(l));
1251
+ if (matches) {
1252
+ for (const r of matches) {
1253
+ result.push({ ...l, ...r });
1254
+ }
1255
+ }
1256
+ }
1257
+ return result;
1258
+ }
1259
+ function distinctFn(data, keyFn) {
1260
+ const seen = /* @__PURE__ */ new Set();
1261
+ return data.filter((row) => {
1262
+ const key = keyFn ? keyFn(row) : JSON.stringify(row);
1263
+ if (seen.has(key)) return false;
1264
+ seen.add(key);
1265
+ return true;
1266
+ });
1267
+ }
1268
+ function transposeFn(data, headerFn, headerFieldName) {
1269
+ if (data.length === 0) return [];
1270
+ const colNames = data.map(
1271
+ (row, i) => headerFn ? String(headerFn(row)) : `col_${i}`
1272
+ );
1273
+ const allFields = /* @__PURE__ */ new Set();
1274
+ for (const row of data) {
1275
+ for (const key of Object.keys(row)) {
1276
+ if (key !== "_group" && key !== headerFieldName) {
1277
+ allFields.add(key);
1278
+ }
1279
+ }
1280
+ }
1281
+ const results = [];
1282
+ for (const field of allFields) {
1283
+ const row = { _field: field };
1284
+ for (let i = 0; i < data.length; i++) {
1285
+ row[colNames[i]] = data[i][field] ?? null;
1286
+ }
1287
+ results.push(row);
1288
+ }
1289
+ return results;
1290
+ }
1291
+ function rollupFn(data, keyFns, aggFns) {
1292
+ const results = [];
1293
+ const numKeys = keyFns.length;
1294
+ for (let level = 0; level <= numKeys; level++) {
1295
+ const activeKeys = keyFns.slice(0, numKeys - level);
1296
+ const groups = groupByFn(data, activeKeys);
1297
+ for (const group of groups) {
1298
+ const row = {};
1299
+ for (const { name } of activeKeys) {
1300
+ row[name] = group.keys[name];
1301
+ }
1302
+ for (let i = numKeys - level; i < numKeys; i++) {
1303
+ row[keyFns[i].name] = null;
1304
+ }
1305
+ const groupedRow = { ...row, _group: group.rows };
1306
+ for (const { fn, name } of aggFns) {
1307
+ row[name] = fn(groupedRow);
1308
+ }
1309
+ row._rollupLevel = level;
1310
+ results.push(row);
1311
+ }
1312
+ }
1313
+ return results;
1314
+ }
1315
+ function defaultForAggregate(name) {
1316
+ if (name === "min" || name === "max") return null;
1317
+ return 0;
1318
+ }
1319
+ function pivotFlatFn(data, pivotFn, aggCompilers, singleAggregate) {
1320
+ const pivotGroups = /* @__PURE__ */ new Map();
1321
+ for (const row of data) {
1322
+ const pivotVal = String(pivotFn(row));
1323
+ if (!pivotGroups.has(pivotVal)) pivotGroups.set(pivotVal, []);
1324
+ pivotGroups.get(pivotVal).push(row);
1325
+ }
1326
+ const result = {};
1327
+ for (const [pivotVal, groupRows] of pivotGroups) {
1328
+ const syntheticRow = { _group: groupRows };
1329
+ const safeName = pivotVal.replace(/\s+/g, "_");
1330
+ for (const { fn, name } of aggCompilers) {
1331
+ const colName = singleAggregate ? safeName : `${name}_${safeName}`;
1332
+ result[colName] = fn(syntheticRow);
1333
+ }
1334
+ }
1335
+ return [result];
1336
+ }
1337
+ function pivotGroupedFn(data, pivotFn, aggCompilers, singleAggregate) {
1338
+ const allPivotValues = /* @__PURE__ */ new Set();
1339
+ for (const groupRow of data) {
1340
+ const groupData = groupRow._group;
1341
+ for (const row of groupData) {
1342
+ allPivotValues.add(String(pivotFn(row)));
1343
+ }
1344
+ }
1345
+ return data.map((groupRow) => {
1346
+ const groupData = groupRow._group;
1347
+ const result = {};
1348
+ for (const key of Object.keys(groupRow)) {
1349
+ if (key !== "_group") result[key] = groupRow[key];
1350
+ }
1351
+ const subGroups = /* @__PURE__ */ new Map();
1352
+ for (const row of groupData) {
1353
+ const pivotVal = String(pivotFn(row));
1354
+ if (!subGroups.has(pivotVal)) subGroups.set(pivotVal, []);
1355
+ subGroups.get(pivotVal).push(row);
1356
+ }
1357
+ for (const pivotVal of allPivotValues) {
1358
+ const subGroupRows = subGroups.get(pivotVal) || [];
1359
+ const syntheticRow = { _group: subGroupRows };
1360
+ const safeName = pivotVal.replace(/\s+/g, "_");
1361
+ for (const { fn, name } of aggCompilers) {
1362
+ const colName = singleAggregate ? safeName : `${name}_${safeName}`;
1363
+ result[colName] = subGroupRows.length > 0 ? fn(syntheticRow) : defaultForAggregate(name);
1364
+ }
1365
+ }
1366
+ return result;
1367
+ });
1368
+ }
1369
+ function computeWindowValues(data, windowDefs) {
1370
+ const n = data.length;
1371
+ const results = Array.from({ length: n }, () => ({}));
1372
+ for (const def of windowDefs) {
1373
+ switch (def.name) {
1374
+ case "row_number":
1375
+ case "running_count":
1376
+ for (let i = 0; i < n; i++) results[i][def.id] = i + 1;
1377
+ break;
1378
+ case "running_sum": {
1379
+ let acc = 0;
1380
+ for (let i = 0; i < n; i++) {
1381
+ acc += Number(def.fieldFn(data[i]));
1382
+ results[i][def.id] = acc;
1383
+ }
1384
+ break;
1385
+ }
1386
+ case "running_avg": {
1387
+ let acc = 0;
1388
+ for (let i = 0; i < n; i++) {
1389
+ acc += Number(def.fieldFn(data[i]));
1390
+ results[i][def.id] = acc / (i + 1);
1391
+ }
1392
+ break;
1393
+ }
1394
+ case "running_min": {
1395
+ let min = Infinity;
1396
+ for (let i = 0; i < n; i++) {
1397
+ const val = Number(def.fieldFn(data[i]));
1398
+ if (val < min) min = val;
1399
+ results[i][def.id] = min;
1400
+ }
1401
+ break;
1402
+ }
1403
+ case "running_max": {
1404
+ let max = -Infinity;
1405
+ for (let i = 0; i < n; i++) {
1406
+ const val = Number(def.fieldFn(data[i]));
1407
+ if (val > max) max = val;
1408
+ results[i][def.id] = max;
1409
+ }
1410
+ break;
1411
+ }
1412
+ case "lag": {
1413
+ const offset = def.offset;
1414
+ for (let i = 0; i < n; i++) {
1415
+ results[i][def.id] = i >= offset ? def.fieldFn(data[i - offset]) : null;
1416
+ }
1417
+ break;
1418
+ }
1419
+ case "lead": {
1420
+ const offset = def.offset;
1421
+ for (let i = 0; i < n; i++) {
1422
+ results[i][def.id] = i + offset < n ? def.fieldFn(data[i + offset]) : null;
1423
+ }
1424
+ break;
1425
+ }
1426
+ }
1427
+ }
1428
+ return results;
1429
+ }
1430
+
1431
+ // ../src/engine/compiler.ts
1432
+ function computeMedian(values) {
1433
+ if (values.length === 0) return 0;
1434
+ const sorted = [...values].sort((a, b) => a - b);
1435
+ const mid = Math.floor(sorted.length / 2);
1436
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1437
+ }
1438
+ function computePercentile(values, p) {
1439
+ if (values.length === 0) return 0;
1440
+ const sorted = [...values].sort((a, b) => a - b);
1441
+ const idx = p / 100 * (sorted.length - 1);
1442
+ const lo = Math.floor(idx);
1443
+ const hi = Math.ceil(idx);
1444
+ if (lo === hi) return sorted[lo];
1445
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
1446
+ }
1447
+ function computeVariance(values) {
1448
+ if (values.length === 0) return 0;
1449
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
1450
+ return values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / values.length;
1451
+ }
1452
+ function computeStddev(values) {
1453
+ return Math.sqrt(computeVariance(values));
1454
+ }
1455
+ function computeSkew(values) {
1456
+ const n = values.length;
1457
+ if (n < 3) return 0;
1458
+ const mean = values.reduce((a, b) => a + b, 0) / n;
1459
+ const sd = computeStddev(values);
1460
+ if (sd === 0) return 0;
1461
+ return values.reduce((acc, v) => acc + ((v - mean) / sd) ** 3, 0) / n;
1462
+ }
1463
+ function computeKurt(values) {
1464
+ const n = values.length;
1465
+ if (n < 4) return 0;
1466
+ const mean = values.reduce((a, b) => a + b, 0) / n;
1467
+ const sd = computeStddev(values);
1468
+ if (sd === 0) return 0;
1469
+ return values.reduce((acc, v) => acc + ((v - mean) / sd) ** 4, 0) / n - 3;
1470
+ }
1471
+ function computeDrawdown(values) {
1472
+ if (values.length === 0) return 0;
1473
+ let peak = values[0];
1474
+ let maxDd = 0;
1475
+ for (const v of values) {
1476
+ if (v > peak) peak = v;
1477
+ const dd = peak === 0 ? 0 : (v - peak) / peak;
1478
+ if (dd < maxDd) maxDd = dd;
1479
+ }
1480
+ return maxDd;
1481
+ }
1482
+ function computeDownsideDeviation(values, target = 0) {
1483
+ if (values.length === 0) return 0;
1484
+ const downside = values.filter((v) => v < target).map((v) => (v - target) ** 2);
1485
+ if (downside.length === 0) return 0;
1486
+ return Math.sqrt(downside.reduce((a, b) => a + b, 0) / values.length);
1487
+ }
1488
+ var WINDOW_FUNCTION_NAMES = /* @__PURE__ */ new Set([
1489
+ "running_sum",
1490
+ "running_avg",
1491
+ "running_count",
1492
+ "running_min",
1493
+ "running_max",
1494
+ "row_number",
1495
+ "lag",
1496
+ "lead"
1497
+ ]);
1498
+ var windowIdCounter = 0;
1499
+ function compileExpression(expr, grouped = false) {
1500
+ switch (expr.kind) {
1501
+ case "NumberLiteral": {
1502
+ const v = expr.value;
1503
+ return () => v;
1504
+ }
1505
+ case "StringLiteral": {
1506
+ const v = expr.value;
1507
+ return () => v;
1508
+ }
1509
+ case "BooleanLiteral": {
1510
+ const v = expr.value;
1511
+ return () => v;
1512
+ }
1513
+ case "NullLiteral":
1514
+ return () => null;
1515
+ case "FieldAccess": {
1516
+ const accessor = getFieldAccessor(expr.path);
1517
+ return accessor;
1518
+ }
1519
+ case "BinaryExpr": {
1520
+ const leftFn = compileExpression(expr.left, grouped);
1521
+ const rightFn = compileExpression(expr.right, grouped);
1522
+ const op = expr.operator;
1523
+ switch (op) {
1524
+ case ">":
1525
+ return (row) => leftFn(row) > rightFn(row);
1526
+ case ">=":
1527
+ return (row) => leftFn(row) >= rightFn(row);
1528
+ case "<":
1529
+ return (row) => leftFn(row) < rightFn(row);
1530
+ case "<=":
1531
+ return (row) => leftFn(row) <= rightFn(row);
1532
+ case "==":
1533
+ return (row) => leftFn(row) === rightFn(row);
1534
+ case "!=":
1535
+ return (row) => leftFn(row) !== rightFn(row);
1536
+ case "&&":
1537
+ return (row) => leftFn(row) && rightFn(row);
1538
+ case "||":
1539
+ return (row) => leftFn(row) || rightFn(row);
1540
+ case "+":
1541
+ return (row) => leftFn(row) + rightFn(row);
1542
+ case "-":
1543
+ return (row) => leftFn(row) - rightFn(row);
1544
+ case "*":
1545
+ return (row) => leftFn(row) * rightFn(row);
1546
+ case "/":
1547
+ return (row) => leftFn(row) / rightFn(row);
1548
+ case "%":
1549
+ return (row) => leftFn(row) % rightFn(row);
1550
+ }
1551
+ break;
1552
+ }
1553
+ case "UnaryExpr": {
1554
+ const operandFn = compileExpression(expr.operand, grouped);
1555
+ if (expr.operator === "!") return (row) => !operandFn(row);
1556
+ return (row) => -operandFn(row);
1557
+ }
1558
+ case "FunctionCall":
1559
+ return compileFunctionCall(expr, grouped);
1560
+ case "AliasExpr":
1561
+ return compileExpression(expr.expression, grouped);
1562
+ }
1563
+ throw new RuntimeError(`Unknown expression kind: ${expr.kind}`);
1564
+ }
1565
+ function compileFunctionCall(expr, grouped) {
1566
+ const { name, args } = expr;
1567
+ if (grouped && AGGREGATE_NAMES.has(name)) {
1568
+ return compileGroupAggregate(name, args);
1569
+ }
1570
+ if (WINDOW_FUNCTION_NAMES.has(name)) {
1571
+ const id = `_w${windowIdCounter++}`;
1572
+ const fieldFn = args.length > 0 ? compileExpression(args[0], false) : void 0;
1573
+ const offset = args.length > 1 ? args[1].value || 1 : 1;
1574
+ const fn = ((row) => {
1575
+ const ctx = row._windowCtx;
1576
+ return ctx?.[id];
1577
+ });
1578
+ fn._windowDef = { id, name, fieldFn, offset };
1579
+ return fn;
1580
+ }
1581
+ switch (name) {
1582
+ case "if": {
1583
+ if (args.length < 2 || args.length > 3) {
1584
+ throw new RuntimeError(`if() expects 2 or 3 arguments, got ${args.length}`);
1585
+ }
1586
+ const condFn = compileExpression(args[0], grouped);
1587
+ const thenFn = compileExpression(args[1], grouped);
1588
+ const elseFn = args.length === 3 ? compileExpression(args[2], grouped) : () => null;
1589
+ return (row) => condFn(row) ? thenFn(row) : elseFn(row);
1590
+ }
1591
+ case "coalesce": {
1592
+ const fns = args.map((a) => compileExpression(a, grouped));
1593
+ return (row) => {
1594
+ for (const fn of fns) {
1595
+ const val = fn(row);
1596
+ if (val != null) return val;
1597
+ }
1598
+ return null;
1599
+ };
1600
+ }
1601
+ case "lower": {
1602
+ const fn = compileExpression(args[0], grouped);
1603
+ return (row) => String(fn(row)).toLowerCase();
1604
+ }
1605
+ case "upper": {
1606
+ const fn = compileExpression(args[0], grouped);
1607
+ return (row) => String(fn(row)).toUpperCase();
1608
+ }
1609
+ case "len": {
1610
+ const fn = compileExpression(args[0], grouped);
1611
+ return (row) => {
1612
+ const val = fn(row);
1613
+ if (Array.isArray(val)) return val.length;
1614
+ if (typeof val === "string") return val.length;
1615
+ return 0;
1616
+ };
1617
+ }
1618
+ case "abs": {
1619
+ const fn = compileExpression(args[0], grouped);
1620
+ return (row) => Math.abs(fn(row));
1621
+ }
1622
+ case "round": {
1623
+ const fn = compileExpression(args[0], grouped);
1624
+ const precFn = args.length > 1 ? compileExpression(args[1], grouped) : () => 0;
1625
+ return (row) => {
1626
+ const val = fn(row);
1627
+ const prec = precFn(row);
1628
+ const factor = Math.pow(10, prec);
1629
+ return Math.round(val * factor) / factor;
1630
+ };
1631
+ }
1632
+ case "concat": {
1633
+ const fns = args.map((a) => compileExpression(a, grouped));
1634
+ return (row) => fns.map((fn) => String(fn(row))).join("");
1635
+ }
1636
+ // ── String functions ────────────────────────────────────
1637
+ case "contains": {
1638
+ if (args.length !== 2) throw new RuntimeError(`contains() expects 2 arguments, got ${args.length}`);
1639
+ const strFn = compileExpression(args[0], grouped);
1640
+ const searchFn = compileExpression(args[1], grouped);
1641
+ return (row) => String(strFn(row)).includes(String(searchFn(row)));
1642
+ }
1643
+ case "startsWith": {
1644
+ if (args.length !== 2) throw new RuntimeError(`startsWith() expects 2 arguments, got ${args.length}`);
1645
+ const strFn = compileExpression(args[0], grouped);
1646
+ const prefixFn = compileExpression(args[1], grouped);
1647
+ return (row) => String(strFn(row)).startsWith(String(prefixFn(row)));
1648
+ }
1649
+ case "endsWith": {
1650
+ if (args.length !== 2) throw new RuntimeError(`endsWith() expects 2 arguments, got ${args.length}`);
1651
+ const strFn = compileExpression(args[0], grouped);
1652
+ const suffixFn = compileExpression(args[1], grouped);
1653
+ return (row) => String(strFn(row)).endsWith(String(suffixFn(row)));
1654
+ }
1655
+ case "trim": {
1656
+ if (args.length !== 1) throw new RuntimeError(`trim() expects 1 argument, got ${args.length}`);
1657
+ const fn = compileExpression(args[0], grouped);
1658
+ return (row) => String(fn(row)).trim();
1659
+ }
1660
+ case "substring": {
1661
+ if (args.length < 2 || args.length > 3) throw new RuntimeError(`substring() expects 2 or 3 arguments, got ${args.length}`);
1662
+ const strFn = compileExpression(args[0], grouped);
1663
+ const startFn = compileExpression(args[1], grouped);
1664
+ const lenFn = args.length === 3 ? compileExpression(args[2], grouped) : void 0;
1665
+ return (row) => {
1666
+ const s = String(strFn(row));
1667
+ const start = Number(startFn(row));
1668
+ if (lenFn) {
1669
+ return s.substring(start, start + Number(lenFn(row)));
1670
+ }
1671
+ return s.substring(start);
1672
+ };
1673
+ }
1674
+ case "replace": {
1675
+ if (args.length !== 3) throw new RuntimeError(`replace() expects 3 arguments, got ${args.length}`);
1676
+ const strFn = compileExpression(args[0], grouped);
1677
+ const searchFn = compileExpression(args[1], grouped);
1678
+ const replaceFn = compileExpression(args[2], grouped);
1679
+ return (row) => String(strFn(row)).replaceAll(String(searchFn(row)), String(replaceFn(row)));
1680
+ }
1681
+ // Non-grouped aggregates used as functions return NaN or undefined — they're
1682
+ // only meaningful as pipeline-level AggregateOps or in grouped selects
1683
+ default:
1684
+ throw new RuntimeError(`Unknown function: ${name}`);
1685
+ }
1686
+ }
1687
+ function compileGroupAggregate(name, args) {
1688
+ const fieldFn = args.length > 0 ? compileExpression(args[0], false) : void 0;
1689
+ const secondFn = args.length > 1 ? compileExpression(args[1], false) : void 0;
1690
+ const extractValues = (row) => row._group.map((r) => Number(fieldFn(r)));
1691
+ switch (name) {
1692
+ // ── Core ───────────────────────────────────────────────
1693
+ case "sum":
1694
+ return (row) => {
1695
+ const group = row._group;
1696
+ return group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
1697
+ };
1698
+ case "avg":
1699
+ return (row) => {
1700
+ const group = row._group;
1701
+ if (group.length === 0) return 0;
1702
+ const total = group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
1703
+ return total / group.length;
1704
+ };
1705
+ case "min":
1706
+ return (row) => {
1707
+ const group = row._group;
1708
+ if (group.length === 0) return null;
1709
+ return group.reduce((min, r) => {
1710
+ const val = fieldFn(r);
1711
+ return val < min ? val : min;
1712
+ }, fieldFn(group[0]));
1713
+ };
1714
+ case "max":
1715
+ return (row) => {
1716
+ const group = row._group;
1717
+ if (group.length === 0) return null;
1718
+ return group.reduce((max, r) => {
1719
+ const val = fieldFn(r);
1720
+ return val > max ? val : max;
1721
+ }, fieldFn(group[0]));
1722
+ };
1723
+ case "count":
1724
+ return (row) => {
1725
+ const group = row._group;
1726
+ return group.length;
1727
+ };
1728
+ // ── Statistical ───────────────────────────────────────
1729
+ case "median":
1730
+ return (row) => computeMedian(extractValues(row));
1731
+ case "stddev":
1732
+ return (row) => computeStddev(extractValues(row));
1733
+ case "var":
1734
+ return (row) => computeVariance(extractValues(row));
1735
+ case "percentile":
1736
+ return (row) => computePercentile(extractValues(row), Number(secondFn(row)));
1737
+ case "skew":
1738
+ return (row) => computeSkew(extractValues(row));
1739
+ case "kurt":
1740
+ return (row) => computeKurt(extractValues(row));
1741
+ // ── Finance ───────────────────────────────────────────
1742
+ case "vwap":
1743
+ return (row) => {
1744
+ const group = row._group;
1745
+ let sumPV = 0, sumV = 0;
1746
+ for (const r of group) {
1747
+ const p = Number(fieldFn(r));
1748
+ const v = Number(secondFn(r));
1749
+ sumPV += p * v;
1750
+ sumV += v;
1751
+ }
1752
+ return sumV === 0 ? 0 : sumPV / sumV;
1753
+ };
1754
+ case "wavg":
1755
+ return (row) => {
1756
+ const group = row._group;
1757
+ let sumWV = 0, sumW = 0;
1758
+ for (const r of group) {
1759
+ const val = Number(fieldFn(r));
1760
+ const w = Number(secondFn(r));
1761
+ sumWV += val * w;
1762
+ sumW += w;
1763
+ }
1764
+ return sumW === 0 ? 0 : sumWV / sumW;
1765
+ };
1766
+ case "drawdown":
1767
+ return (row) => computeDrawdown(extractValues(row));
1768
+ // ── Ratios ────────────────────────────────────────────
1769
+ case "pct":
1770
+ return (row) => {
1771
+ const group = row._group;
1772
+ return group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
1773
+ };
1774
+ case "sharpe":
1775
+ return (row) => {
1776
+ const vals = extractValues(row);
1777
+ if (vals.length === 0) return 0;
1778
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
1779
+ const sd = computeStddev(vals);
1780
+ return sd === 0 ? 0 : mean / sd;
1781
+ };
1782
+ case "calmar":
1783
+ return (row) => {
1784
+ const vals = extractValues(row);
1785
+ if (vals.length === 0) return 0;
1786
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
1787
+ const dd = Math.abs(computeDrawdown(vals));
1788
+ return dd === 0 ? 0 : mean / dd;
1789
+ };
1790
+ case "sortino":
1791
+ return (row) => {
1792
+ const vals = extractValues(row);
1793
+ if (vals.length === 0) return 0;
1794
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
1795
+ const dsd = computeDownsideDeviation(vals, 0);
1796
+ return dsd === 0 ? 0 : mean / dsd;
1797
+ };
1798
+ case "info_ratio":
1799
+ return (row) => {
1800
+ const group = row._group;
1801
+ const excess = group.map((r) => Number(fieldFn(r)) - Number(secondFn(r)));
1802
+ if (excess.length === 0) return 0;
1803
+ const mean = excess.reduce((a, b) => a + b, 0) / excess.length;
1804
+ const sd = computeStddev(excess);
1805
+ return sd === 0 ? 0 : mean / sd;
1806
+ };
1807
+ // ── Counting ──────────────────────────────────────────
1808
+ case "distinct_count":
1809
+ return (row) => {
1810
+ const group = row._group;
1811
+ return new Set(group.map((r) => fieldFn(r))).size;
1812
+ };
1813
+ case "sum_abs":
1814
+ return (row) => {
1815
+ const group = row._group;
1816
+ return group.reduce((acc, r) => acc + Math.abs(Number(fieldFn(r))), 0);
1817
+ };
1818
+ case "abs_sum":
1819
+ return (row) => {
1820
+ const group = row._group;
1821
+ return Math.abs(group.reduce((acc, r) => acc + Number(fieldFn(r)), 0));
1822
+ };
1823
+ // ── Range ─────────────────────────────────────────────
1824
+ case "first_value":
1825
+ return (row) => {
1826
+ const group = row._group;
1827
+ return group.length > 0 ? fieldFn(group[0]) : null;
1828
+ };
1829
+ case "last_value":
1830
+ return (row) => {
1831
+ const group = row._group;
1832
+ return group.length > 0 ? fieldFn(group[group.length - 1]) : null;
1833
+ };
1834
+ default:
1835
+ throw new RuntimeError(`Unknown aggregate function: ${name}`);
1836
+ }
1837
+ }
1838
+ function resolveFieldName(expr) {
1839
+ switch (expr.kind) {
1840
+ case "AliasExpr":
1841
+ return expr.alias;
1842
+ case "FieldAccess":
1843
+ return expr.path[expr.path.length - 1];
1844
+ case "FunctionCall":
1845
+ return expr.name;
1846
+ default:
1847
+ return "value";
1848
+ }
1849
+ }
1850
+ function compileOperation(op, isGrouped) {
1851
+ switch (op.kind) {
1852
+ case "WhereOp": {
1853
+ const condFn = compileExpression(op.condition);
1854
+ return {
1855
+ fn: (data) => data.filter((row) => condFn(row)),
1856
+ producesGrouped: false
1857
+ };
1858
+ }
1859
+ case "SelectOp":
1860
+ return compileSelectOp(op, isGrouped);
1861
+ case "SortOp": {
1862
+ const criteria = op.criteria.map((c) => ({
1863
+ fn: compileExpression(c.expression),
1864
+ dir: c.direction === "desc" ? -1 : 1
1865
+ }));
1866
+ return {
1867
+ fn: (data) => {
1868
+ return [...data].sort((a, b) => {
1869
+ for (const { fn, dir } of criteria) {
1870
+ const cmp = compareValues(fn(a), fn(b));
1871
+ if (cmp !== 0) return cmp * dir;
1872
+ }
1873
+ return 0;
1874
+ });
1875
+ },
1876
+ producesGrouped: false
1877
+ };
1878
+ }
1879
+ case "GroupByOp": {
1880
+ const keyFns = op.keys.map((k) => {
1881
+ const fn = compileExpression(k);
1882
+ const name = k.kind === "FieldAccess" ? k.path[k.path.length - 1] : "key";
1883
+ return { fn, name };
1884
+ });
1885
+ return {
1886
+ fn: (data) => {
1887
+ const groups = groupByFn(data, keyFns);
1888
+ return groups.map((g) => ({ ...g.keys, _group: g.rows }));
1889
+ },
1890
+ producesGrouped: true
1891
+ };
1892
+ }
1893
+ case "JoinOp": {
1894
+ const rightSource = op.right;
1895
+ if (op.condition.kind === "BinaryExpr" && op.condition.operator === "==" && op.condition.left.kind === "FieldAccess" && op.condition.right.kind === "FieldAccess") {
1896
+ const leftKeyFn = compileExpression(op.condition.left);
1897
+ const rightKeyFn = compileExpression(op.condition.right);
1898
+ return {
1899
+ fn: (data, context) => {
1900
+ const rightData = context[rightSource];
1901
+ if (!rightData) {
1902
+ throw new RuntimeError(`Join source "${rightSource}" not found in context`);
1903
+ }
1904
+ return hashJoin(data, rightData, leftKeyFn, rightKeyFn);
1905
+ },
1906
+ producesGrouped: false
1907
+ };
1908
+ }
1909
+ const condFn = compileExpression(op.condition);
1910
+ return {
1911
+ fn: (data, context) => {
1912
+ const rightData = context[rightSource];
1913
+ if (!rightData) {
1914
+ throw new RuntimeError(`Join source "${rightSource}" not found in context`);
1915
+ }
1916
+ return nestedLoopJoin(data, rightData, condFn);
1917
+ },
1918
+ producesGrouped: false
1919
+ };
1920
+ }
1921
+ case "FirstOp": {
1922
+ const count = op.count;
1923
+ return {
1924
+ fn: (data) => data.slice(0, count),
1925
+ producesGrouped: false
1926
+ };
1927
+ }
1928
+ case "LastOp": {
1929
+ const count = op.count;
1930
+ return {
1931
+ fn: (data) => data.slice(-count),
1932
+ producesGrouped: false
1933
+ };
1934
+ }
1935
+ case "DistinctOp": {
1936
+ const keyFn = op.fields ? (() => {
1937
+ const fns = op.fields.map((f) => compileExpression(f));
1938
+ return (row) => JSON.stringify(fns.map((fn) => fn(row)));
1939
+ })() : void 0;
1940
+ return {
1941
+ fn: (data) => distinctFn(data, keyFn),
1942
+ producesGrouped: false
1943
+ };
1944
+ }
1945
+ case "FlattenOp": {
1946
+ if (op.field) {
1947
+ const fieldFn = compileExpression(op.field);
1948
+ return {
1949
+ fn: (data) => data.flatMap((row) => {
1950
+ const val = fieldFn(row);
1951
+ return Array.isArray(val) ? val : [val];
1952
+ }),
1953
+ producesGrouped: false
1954
+ };
1955
+ }
1956
+ return {
1957
+ fn: (data) => data.flat(),
1958
+ producesGrouped: false
1959
+ };
1960
+ }
1961
+ case "TransposeOp": {
1962
+ const headerFn = op.headerField ? compileExpression(op.headerField) : void 0;
1963
+ const headerFieldName = op.headerField ? resolveFieldName(op.headerField) : void 0;
1964
+ return {
1965
+ fn: (data) => transposeFn(data, headerFn, headerFieldName),
1966
+ producesGrouped: false
1967
+ };
1968
+ }
1969
+ case "RollupOp": {
1970
+ const keyFns = op.keys.map((k) => {
1971
+ const fn = compileExpression(k);
1972
+ const name = resolveFieldName(k);
1973
+ return { fn, name };
1974
+ });
1975
+ const aggFns = op.aggregates.map((a) => {
1976
+ const fn = compileExpression(a, true);
1977
+ const name = resolveFieldName(a);
1978
+ return { fn, name };
1979
+ });
1980
+ const pctCols = /* @__PURE__ */ new Set();
1981
+ for (const a of op.aggregates) {
1982
+ const expr = a.kind === "AliasExpr" ? a.expression : a;
1983
+ if (expr.kind === "FunctionCall" && expr.name === "pct") {
1984
+ pctCols.add(resolveFieldName(a));
1985
+ }
1986
+ }
1987
+ return {
1988
+ fn: (data) => {
1989
+ const results = rollupFn(data, keyFns, aggFns);
1990
+ if (pctCols.size > 0) {
1991
+ const numKeys = keyFns.length;
1992
+ const grandRow = results.find((r) => r._rollupLevel === numKeys);
1993
+ for (const col of pctCols) {
1994
+ const total = Number(grandRow?.[col]) || 1;
1995
+ for (const row of results) {
1996
+ row[col] = Number(row[col]) / total * 100;
1997
+ }
1998
+ }
1999
+ if (pctCols.size === aggFns.length) {
2000
+ return results.filter((r) => r._rollupLevel !== numKeys);
2001
+ }
2002
+ }
2003
+ return results;
2004
+ },
2005
+ producesGrouped: false
2006
+ };
2007
+ }
2008
+ case "PivotOp": {
2009
+ const pivotFn = compileExpression(op.pivotField);
2010
+ const aggCompilers = op.aggregates.map((a) => ({
2011
+ fn: compileExpression(a, true),
2012
+ // grouped = true
2013
+ name: resolveFieldName(a)
2014
+ }));
2015
+ const singleAggregate = aggCompilers.length === 1;
2016
+ return {
2017
+ fn: (data) => {
2018
+ if (isGrouped) {
2019
+ return pivotGroupedFn(data, pivotFn, aggCompilers, singleAggregate);
2020
+ }
2021
+ return pivotFlatFn(data, pivotFn, aggCompilers, singleAggregate);
2022
+ },
2023
+ producesGrouped: false
2024
+ };
2025
+ }
2026
+ case "MapOp": {
2027
+ const fieldCompilers = op.fields.map((field) => {
2028
+ const name = resolveFieldName(field);
2029
+ const fn = compileExpression(field, isGrouped);
2030
+ return { name, fn };
2031
+ });
2032
+ return {
2033
+ fn: (data) => data.map((row) => {
2034
+ const result = { ...row };
2035
+ for (const { name, fn } of fieldCompilers) {
2036
+ result[name] = fn(row);
2037
+ }
2038
+ return result;
2039
+ }),
2040
+ producesGrouped: isGrouped
2041
+ };
2042
+ }
2043
+ case "ReduceOp": {
2044
+ const initialFn = compileExpression(op.initial);
2045
+ const accFn = compileExpression(op.accumulator);
2046
+ return {
2047
+ fn: (data) => {
2048
+ let acc = initialFn({});
2049
+ for (const row of data) {
2050
+ acc = accFn({ ...row, _acc: acc });
2051
+ }
2052
+ return acc;
2053
+ },
2054
+ producesGrouped: false
2055
+ };
2056
+ }
2057
+ case "AggregateOp": {
2058
+ const argFns = op.args?.map((a) => compileExpression(a));
2059
+ const primaryFn = argFns ? argFns[0] : op.field ? compileExpression(op.field) : void 0;
2060
+ const secondFn = argFns ? argFns[1] : void 0;
2061
+ const aggName = op.function;
2062
+ return {
2063
+ fn: (data) => {
2064
+ const vals = () => data.map((row) => Number(primaryFn(row)));
2065
+ switch (aggName) {
2066
+ // ── Core ──────────────────────────────────────
2067
+ case "count":
2068
+ return data.length;
2069
+ case "sum":
2070
+ return data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
2071
+ case "avg": {
2072
+ if (data.length === 0) return 0;
2073
+ const total = data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
2074
+ return total / data.length;
2075
+ }
2076
+ case "min":
2077
+ return data.reduce((min, row) => {
2078
+ const val = primaryFn(row);
2079
+ return val < min ? val : min;
2080
+ }, primaryFn(data[0]));
2081
+ case "max":
2082
+ return data.reduce((max, row) => {
2083
+ const val = primaryFn(row);
2084
+ return val > max ? val : max;
2085
+ }, primaryFn(data[0]));
2086
+ // ── Statistical ───────────────────────────────
2087
+ case "median":
2088
+ return computeMedian(vals());
2089
+ case "stddev":
2090
+ return computeStddev(vals());
2091
+ case "var":
2092
+ return computeVariance(vals());
2093
+ case "percentile":
2094
+ return computePercentile(vals(), Number(secondFn(data[0])));
2095
+ case "skew":
2096
+ return computeSkew(vals());
2097
+ case "kurt":
2098
+ return computeKurt(vals());
2099
+ // ── Finance ───────────────────────────────────
2100
+ case "vwap": {
2101
+ let sumPV = 0, sumV = 0;
2102
+ for (const row of data) {
2103
+ const p = Number(primaryFn(row));
2104
+ const v = Number(secondFn(row));
2105
+ sumPV += p * v;
2106
+ sumV += v;
2107
+ }
2108
+ return sumV === 0 ? 0 : sumPV / sumV;
2109
+ }
2110
+ case "wavg": {
2111
+ let sumWV = 0, sumW = 0;
2112
+ for (const row of data) {
2113
+ const val = Number(primaryFn(row));
2114
+ const w = Number(secondFn(row));
2115
+ sumWV += val * w;
2116
+ sumW += w;
2117
+ }
2118
+ return sumW === 0 ? 0 : sumWV / sumW;
2119
+ }
2120
+ case "drawdown":
2121
+ return computeDrawdown(vals());
2122
+ // ── Ratios ────────────────────────────────────
2123
+ case "pct": {
2124
+ return data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
2125
+ }
2126
+ case "sharpe": {
2127
+ const v = vals();
2128
+ if (v.length === 0) return 0;
2129
+ const mean = v.reduce((a, b) => a + b, 0) / v.length;
2130
+ const sd = computeStddev(v);
2131
+ return sd === 0 ? 0 : mean / sd;
2132
+ }
2133
+ case "calmar": {
2134
+ const v = vals();
2135
+ if (v.length === 0) return 0;
2136
+ const mean = v.reduce((a, b) => a + b, 0) / v.length;
2137
+ const dd = Math.abs(computeDrawdown(v));
2138
+ return dd === 0 ? 0 : mean / dd;
2139
+ }
2140
+ case "sortino": {
2141
+ const v = vals();
2142
+ if (v.length === 0) return 0;
2143
+ const mean = v.reduce((a, b) => a + b, 0) / v.length;
2144
+ const dsd = computeDownsideDeviation(v, 0);
2145
+ return dsd === 0 ? 0 : mean / dsd;
2146
+ }
2147
+ case "info_ratio": {
2148
+ const excess = data.map((row) => Number(primaryFn(row)) - Number(secondFn(row)));
2149
+ if (excess.length === 0) return 0;
2150
+ const mean = excess.reduce((a, b) => a + b, 0) / excess.length;
2151
+ const sd = computeStddev(excess);
2152
+ return sd === 0 ? 0 : mean / sd;
2153
+ }
2154
+ // ── Counting ──────────────────────────────────
2155
+ case "distinct_count":
2156
+ return new Set(data.map((row) => primaryFn(row))).size;
2157
+ case "sum_abs":
2158
+ return data.reduce((acc, row) => acc + Math.abs(Number(primaryFn(row))), 0);
2159
+ case "abs_sum":
2160
+ return Math.abs(data.reduce((acc, row) => acc + Number(primaryFn(row)), 0));
2161
+ // ── Range ─────────────────────────────────────
2162
+ case "first_value":
2163
+ return data.length > 0 ? primaryFn(data[0]) : null;
2164
+ case "last_value":
2165
+ return data.length > 0 ? primaryFn(data[data.length - 1]) : null;
2166
+ }
2167
+ },
2168
+ producesGrouped: false
2169
+ };
2170
+ }
2171
+ }
2172
+ }
2173
+ function compileSelectOp(op, isGrouped) {
2174
+ const fieldCompilers = op.fields.map((field) => {
2175
+ const name = resolveFieldName(field);
2176
+ const fn = compileExpression(field, isGrouped);
2177
+ return { name, fn };
2178
+ });
2179
+ const windowDefs = [];
2180
+ for (const { fn } of fieldCompilers) {
2181
+ const wDef = fn._windowDef;
2182
+ if (wDef) windowDefs.push(wDef);
2183
+ }
2184
+ if (windowDefs.length > 0) {
2185
+ return {
2186
+ fn: (data) => {
2187
+ const windowValues = computeWindowValues(data, windowDefs);
2188
+ return data.map((row, i) => {
2189
+ const enrichedRow = { ...row, _windowCtx: windowValues[i] };
2190
+ const result = {};
2191
+ for (const { name, fn } of fieldCompilers) {
2192
+ result[name] = fn(enrichedRow);
2193
+ }
2194
+ return result;
2195
+ });
2196
+ },
2197
+ producesGrouped: false
2198
+ };
2199
+ }
2200
+ return {
2201
+ fn: (data) => {
2202
+ return data.map((row) => {
2203
+ const result = {};
2204
+ for (const { name, fn } of fieldCompilers) {
2205
+ result[name] = fn(row);
2206
+ }
2207
+ return result;
2208
+ });
2209
+ },
2210
+ producesGrouped: false
2211
+ };
2212
+ }
2213
+ function compile(ast, source2) {
2214
+ const compiledOps = [];
2215
+ let isGrouped = false;
2216
+ for (const op of ast.operations) {
2217
+ const { fn, producesGrouped } = compileOperation(op, isGrouped);
2218
+ compiledOps.push(fn);
2219
+ isGrouped = producesGrouped;
2220
+ }
2221
+ const sourceName = ast.source;
2222
+ const queryFn = (context) => {
2223
+ const sourceData = context[sourceName];
2224
+ if (!sourceData) {
2225
+ throw new RuntimeError(`Source "${sourceName}" not found in context`);
2226
+ }
2227
+ if (!Array.isArray(sourceData)) {
2228
+ throw new RuntimeError(`Source "${sourceName}" is not an array`);
2229
+ }
2230
+ let data = sourceData;
2231
+ for (const opFn of compiledOps) {
2232
+ if (!Array.isArray(data)) {
2233
+ return data;
2234
+ }
2235
+ data = opFn(data, context);
2236
+ }
2237
+ return data;
2238
+ };
2239
+ const compiled = queryFn;
2240
+ Object.defineProperty(compiled, "source", { value: source2, writable: false });
2241
+ Object.defineProperty(compiled, "ast", { value: ast, writable: false });
2242
+ return compiled;
2243
+ }
2244
+
2245
+ // ../src/engine/cache.ts
2246
+ var QueryCache = class {
2247
+ cache = /* @__PURE__ */ new Map();
2248
+ maxSize;
2249
+ constructor(maxSize = 128) {
2250
+ this.maxSize = maxSize;
2251
+ }
2252
+ get(key) {
2253
+ const value = this.cache.get(key);
2254
+ if (value) {
2255
+ this.cache.delete(key);
2256
+ this.cache.set(key, value);
2257
+ }
2258
+ return value;
2259
+ }
2260
+ set(key, value) {
2261
+ if (this.cache.has(key)) {
2262
+ this.cache.delete(key);
2263
+ } else if (this.cache.size >= this.maxSize) {
2264
+ const firstKey = this.cache.keys().next().value;
2265
+ if (firstKey !== void 0) {
2266
+ this.cache.delete(firstKey);
2267
+ }
2268
+ }
2269
+ this.cache.set(key, value);
2270
+ }
2271
+ clear() {
2272
+ this.cache.clear();
2273
+ }
2274
+ get size() {
2275
+ return this.cache.size;
2276
+ }
2277
+ };
2278
+
2279
+ // ../src/engine/index.ts
2280
+ var defaultCache = new QueryCache(128);
2281
+ var OPERATION_KEYWORDS = /* @__PURE__ */ new Set([
2282
+ "where",
2283
+ "select",
2284
+ "sort",
2285
+ "groupBy",
2286
+ "join",
2287
+ "first",
2288
+ "last",
2289
+ "distinct",
2290
+ "map",
2291
+ "reduce",
2292
+ "rollup",
2293
+ "pivot",
2294
+ "flatten",
2295
+ "transpose"
2296
+ ]);
2297
+ function normalizeExpression(expression) {
2298
+ const trimmed = expression.trimStart();
2299
+ const match = trimmed.match(/^(\w+)\s*\(/);
2300
+ if (match && OPERATION_KEYWORDS.has(match[1])) {
2301
+ return `_data | ${trimmed}`;
2302
+ }
2303
+ return expression;
2304
+ }
2305
+ function compile2(expression, useCache = true) {
2306
+ if (useCache) {
2307
+ const cached = defaultCache.get(expression);
2308
+ if (cached) return cached;
2309
+ }
2310
+ const tokens = tokenize(expression);
2311
+ const ast = parse(tokens);
2312
+ const fn = compile(ast, expression);
2313
+ if (useCache) {
2314
+ defaultCache.set(expression, fn);
2315
+ }
2316
+ return fn;
2317
+ }
2318
+ function query(context, expression) {
2319
+ if (Array.isArray(context)) {
2320
+ const normalized = normalizeExpression(expression);
2321
+ const fn2 = compile2(normalized);
2322
+ return fn2({ _data: context });
2323
+ }
2324
+ const fn = compile2(expression);
2325
+ return fn(context);
2326
+ }
2327
+
2328
+ // src/server/routes/control.ts
2329
+ function registerControlRoutes(app, sourceManager, endpointRegistry, configCallbacks) {
2330
+ app.get("/api/_control/sources", async () => {
2331
+ return sourceManager.getAllStatuses();
2332
+ });
2333
+ app.post("/api/_control/sources", async (req) => {
2334
+ const { name, config } = req.body;
2335
+ await sourceManager.addSource(name, config);
2336
+ await configCallbacks.onSourceChange(name, config);
2337
+ return { ok: true, name, status: sourceManager.getSourceStatus(name) };
2338
+ });
2339
+ app.delete("/api/_control/sources/:name", async (req) => {
2340
+ const { name } = req.params;
2341
+ const removed = sourceManager.removeSource(name);
2342
+ if (!removed) return { ok: false, error: `Source "${name}" not found` };
2343
+ await configCallbacks.onSourceChange(name, null);
2344
+ return { ok: true, name };
2345
+ });
2346
+ app.post("/api/_control/sources/:name/test", async (req) => {
2347
+ const { name } = req.params;
2348
+ const data = sourceManager.getSourceData(name);
2349
+ if (!data) return { ok: false, error: `Source "${name}" not found` };
2350
+ return {
2351
+ ok: true,
2352
+ rowCount: data.length,
2353
+ sample: data.slice(0, 3),
2354
+ status: sourceManager.getSourceStatus(name)
2355
+ };
2356
+ });
2357
+ app.get("/api/_control/endpoints", async () => {
2358
+ const result = {};
2359
+ for (const [path, config] of endpointRegistry.endpoints) {
2360
+ result[path] = config;
2361
+ }
2362
+ return result;
2363
+ });
2364
+ app.post("/api/_control/endpoints", async (req) => {
2365
+ const { path, config } = req.body;
2366
+ endpointRegistry.endpoints.set(path, config);
2367
+ endpointRegistry.onAdd?.(path, config);
2368
+ await configCallbacks.onEndpointChange(path, config);
2369
+ return { ok: true, path };
2370
+ });
2371
+ app.delete("/api/_control/endpoints", async (req) => {
2372
+ const path = req.body.path;
2373
+ const removed = endpointRegistry.endpoints.delete(path);
2374
+ if (!removed) return { ok: false, error: `Endpoint "${path}" not found` };
2375
+ endpointRegistry.onRemove?.(path);
2376
+ await configCallbacks.onEndpointChange(path, null);
2377
+ return { ok: true, path };
2378
+ });
2379
+ app.post("/api/_control/refresh", async () => {
2380
+ await sourceManager.refreshAll();
2381
+ return { ok: true };
2382
+ });
2383
+ app.post("/api/_control/query", async (req) => {
2384
+ const { query: expr } = req.body;
2385
+ const context = sourceManager.getContext();
2386
+ try {
2387
+ const result = query(context, expr);
2388
+ return { ok: true, result };
2389
+ } catch (err) {
2390
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2391
+ }
2392
+ });
2393
+ }
2394
+
2395
+ // src/server/cache.ts
2396
+ var ResponseCache = class {
2397
+ entries = /* @__PURE__ */ new Map();
2398
+ get(key) {
2399
+ const entry = this.entries.get(key);
2400
+ if (!entry) return void 0;
2401
+ if (Date.now() > entry.expiresAt) {
2402
+ this.entries.delete(key);
2403
+ return void 0;
2404
+ }
2405
+ return entry.data;
2406
+ }
2407
+ set(key, data, ttlMs) {
2408
+ this.entries.set(key, { data, expiresAt: Date.now() + ttlMs });
2409
+ }
2410
+ invalidate(key) {
2411
+ this.entries.delete(key);
2412
+ }
2413
+ clear() {
2414
+ this.entries.clear();
2415
+ }
2416
+ };
2417
+
2418
+ // src/server/routes/dynamic.ts
2419
+ function registerDynamicRoutes(app, sourceManager, endpoints) {
2420
+ const cache = new ResponseCache();
2421
+ app.addHook("onRequest", async (req, reply) => {
2422
+ const path = req.url.split("?")[0];
2423
+ if (!path.startsWith("/api/") || path.startsWith("/api/_control")) return;
2424
+ const config = endpoints.get(path);
2425
+ if (!config) return;
2426
+ if (config.cache) {
2427
+ const cached = cache.get(path);
2428
+ if (cached !== void 0) {
2429
+ reply.send(cached);
2430
+ return;
2431
+ }
2432
+ }
2433
+ const context = sourceManager.getContext();
2434
+ try {
2435
+ const result = query(context, config.query);
2436
+ if (config.cache) {
2437
+ cache.set(path, result, parseDuration(config.cache));
2438
+ }
2439
+ reply.send(result);
2440
+ } catch (err) {
2441
+ reply.status(500).send({
2442
+ error: err instanceof Error ? err.message : String(err)
2443
+ });
2444
+ }
2445
+ });
2446
+ }
2447
+
2448
+ // src/server/routes/sse.ts
2449
+ function registerSSERoutes(app, sourceManager) {
2450
+ app.get("/api/_control/sse", async (req, reply) => {
2451
+ const queriesParam = req.query.queries;
2452
+ if (!queriesParam) {
2453
+ return reply.status(400).send({ error: "queries parameter required (JSON array of strings)" });
2454
+ }
2455
+ let queries;
2456
+ try {
2457
+ queries = JSON.parse(queriesParam);
2458
+ if (!Array.isArray(queries)) throw new Error();
2459
+ } catch {
2460
+ return reply.status(400).send({ error: "queries must be a JSON array of strings" });
2461
+ }
2462
+ reply.raw.writeHead(200, {
2463
+ "Content-Type": "text/event-stream",
2464
+ "Cache-Control": "no-cache",
2465
+ "Connection": "keep-alive",
2466
+ "Access-Control-Allow-Origin": "*"
2467
+ });
2468
+ sendAllPanels(reply.raw, queries, sourceManager);
2469
+ let pending = false;
2470
+ let timer = null;
2471
+ const throttledSend = () => {
2472
+ if (timer) {
2473
+ pending = true;
2474
+ return;
2475
+ }
2476
+ sendAllPanels(reply.raw, queries, sourceManager);
2477
+ timer = setTimeout(() => {
2478
+ timer = null;
2479
+ if (pending) {
2480
+ pending = false;
2481
+ throttledSend();
2482
+ }
2483
+ }, 1e3);
2484
+ };
2485
+ const unsubscribe = sourceManager.onSourceUpdate(throttledSend);
2486
+ const heartbeat = setInterval(() => {
2487
+ reply.raw.write(":heartbeat\n\n");
2488
+ }, 15e3);
2489
+ req.raw.on("close", () => {
2490
+ unsubscribe();
2491
+ clearInterval(heartbeat);
2492
+ if (timer) clearTimeout(timer);
2493
+ });
2494
+ await reply.hijack();
2495
+ });
2496
+ }
2497
+ function sendAllPanels(res, queries, sourceManager) {
2498
+ const context = sourceManager.getContext();
2499
+ for (let i = 0; i < queries.length; i++) {
2500
+ try {
2501
+ const result = query(context, queries[i]);
2502
+ const event = JSON.stringify({ panelIndex: i, result });
2503
+ res.write(`data: ${event}
2504
+
2505
+ `);
2506
+ } catch (err) {
2507
+ const event = JSON.stringify({ panelIndex: i, error: err instanceof Error ? err.message : String(err) });
2508
+ res.write(`data: ${event}
2509
+
2510
+ `);
2511
+ }
2512
+ }
2513
+ }
2514
+
2515
+ // src/server/activity-log.ts
2516
+ var MAX_ENTRIES = 100;
2517
+ var entries = [];
2518
+ function logActivity(level, message) {
2519
+ entries.push({
2520
+ time: (/* @__PURE__ */ new Date()).toISOString(),
2521
+ level,
2522
+ message
2523
+ });
2524
+ if (entries.length > MAX_ENTRIES) {
2525
+ entries.splice(0, entries.length - MAX_ENTRIES);
2526
+ }
2527
+ }
2528
+ function getActivityLog(since) {
2529
+ if (!since) return [...entries];
2530
+ return entries.filter((e) => e.time > since);
2531
+ }
2532
+ function info(msg) {
2533
+ logActivity("info", msg);
2534
+ }
2535
+ function warn(msg) {
2536
+ logActivity("warn", msg);
2537
+ }
2538
+ function error(msg) {
2539
+ logActivity("error", msg);
2540
+ }
2541
+
2542
+ // src/server/app.ts
2543
+ async function createServer(config, configPath, cwd) {
2544
+ const app = Fastify({ logger: false });
2545
+ await app.register(cors);
2546
+ const sourceManager = new SourceManager(cwd);
2547
+ const startedAt = /* @__PURE__ */ new Date();
2548
+ info("Server starting...");
2549
+ for (const [name, sourceConfig] of Object.entries(config.sources)) {
2550
+ try {
2551
+ await sourceManager.addSource(name, sourceConfig);
2552
+ info(`Source "${name}" loaded (${sourceConfig.type})`);
2553
+ } catch (err) {
2554
+ const msg = err instanceof Error ? err.message : String(err);
2555
+ error(`Source "${name}" failed: ${msg}`);
2556
+ console.error(`Failed to start source "${name}":`, msg);
2557
+ }
2558
+ }
2559
+ const endpoints = /* @__PURE__ */ new Map();
2560
+ for (const [path, endpointConfig] of Object.entries(config.endpoints)) {
2561
+ endpoints.set(path, endpointConfig);
2562
+ info(`Endpoint registered: ${path}`);
2563
+ }
2564
+ const configCallbacks = {
2565
+ onSourceChange: async (name, sourceConfig) => {
2566
+ if (sourceConfig) {
2567
+ config.sources[name] = sourceConfig;
2568
+ info(`Source "${name}" added (${sourceConfig.type})`);
2569
+ } else {
2570
+ delete config.sources[name];
2571
+ warn(`Source "${name}" removed`);
2572
+ }
2573
+ await saveConfig(config, configPath);
2574
+ },
2575
+ onEndpointChange: async (path, endpointConfig) => {
2576
+ if (endpointConfig) {
2577
+ config.endpoints[path] = endpointConfig;
2578
+ info(`Endpoint "${path}" added`);
2579
+ } else {
2580
+ delete config.endpoints[path];
2581
+ warn(`Endpoint "${path}" removed`);
2582
+ }
2583
+ await saveConfig(config, configPath);
2584
+ }
2585
+ };
2586
+ app.addHook("onResponse", async (request, reply) => {
2587
+ const url = request.url;
2588
+ if (url === "/health" || url === "/status" || url === "/api/_control/activity") return;
2589
+ if (url === "/api/_control/sources" && request.method === "GET") return;
2590
+ if (url === "/api/_control/endpoints" && request.method === "GET") return;
2591
+ const status = reply.statusCode;
2592
+ const level = status >= 400 ? "error" : "info";
2593
+ logActivity(level, `${request.method} ${url} \u2192 ${status}`);
2594
+ });
2595
+ app.get("/api/_control/activity", async (req) => {
2596
+ return getActivityLog(req.query.since);
2597
+ });
2598
+ registerHealthRoutes(app, sourceManager, startedAt);
2599
+ registerControlRoutes(app, sourceManager, { endpoints }, configCallbacks);
2600
+ registerSSERoutes(app, sourceManager);
2601
+ registerDynamicRoutes(app, sourceManager, endpoints);
2602
+ info("Server ready");
2603
+ return { app, sourceManager, endpoints };
2604
+ }
2605
+
2606
+ // src/utils/banner.ts
2607
+ import chalk3 from "chalk";
2608
+ var LOGO = [
2609
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557",
2610
+ " \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D",
2611
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2554\u255D ",
2612
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u2584\u2584 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u255A\u2588\u2588\u2554\u255D ",
2613
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ",
2614
+ " \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2580\u2580\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D "
2615
+ ];
2616
+ var GRADIENT_COLORS = [
2617
+ "#06b6d4",
2618
+ // cyan
2619
+ "#0891b2",
2620
+ "#0e7490",
2621
+ "#2563eb",
2622
+ // blue
2623
+ "#4f46e5",
2624
+ "#7c3aed"
2625
+ // violet
2626
+ ];
2627
+ function applyGradient(line, colorIndex) {
2628
+ const color = GRADIENT_COLORS[colorIndex % GRADIENT_COLORS.length];
2629
+ return chalk3.hex(color)(line);
2630
+ }
2631
+ function printBanner() {
2632
+ console.log();
2633
+ LOGO.forEach((line, i) => {
2634
+ console.log(applyGradient(line, i));
2635
+ });
2636
+ console.log();
2637
+ console.log(chalk3.dim(` v0.1.0 \u2014 data sources, aggregations, APIs, and terminal dashboards`));
2638
+ console.log();
2639
+ }
2640
+ function printCompactBanner() {
2641
+ console.log();
2642
+ console.log(
2643
+ chalk3.hex("#06b6d4").bold(" \u2554\u2550\u2557 \u2566 \u2554\u2550\u2557 \u2554\u2550\u2557 \u2554\u2550\u2557 \u2566 \u2566 \u2554\u2550\u2557 \u2566\u2550\u2557 \u2566 \u2566")
2644
+ );
2645
+ console.log(
2646
+ chalk3.hex("#4f46e5").bold(" \u2560\u2550\u255D \u2551 \u2560\u2550\u255D \u2551\u2563 \u2551\u2550\u256C\u2557\u2551 \u2551 \u2551\u2563 \u2560\u2566\u255D \u255A\u2566\u255D")
2647
+ );
2648
+ console.log(
2649
+ chalk3.hex("#7c3aed").bold(" \u2569 \u2569 \u2569 \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u255A\u2550\u255D \u255A\u2550\u255D \u2569\u255A\u2550 \u2569 ")
2650
+ );
2651
+ console.log();
2652
+ }
2653
+ function printServerStartup(opts) {
2654
+ const { url, sourceCount, endpointCount } = opts;
2655
+ console.log(chalk3.hex("#06b6d4").bold(" \u25CF Server running"));
2656
+ console.log();
2657
+ console.log(` ${chalk3.dim("URL")} ${chalk3.white.bold(url)}`);
2658
+ console.log(` ${chalk3.dim("Sources")} ${chalk3.white(String(sourceCount))} loaded`);
2659
+ console.log(` ${chalk3.dim("Endpoints")} ${chalk3.white(String(endpointCount))} registered`);
2660
+ console.log();
2661
+ console.log(chalk3.dim(" Press Ctrl+C to stop"));
2662
+ console.log();
2663
+ }
2664
+
2665
+ // src/commands/serve.ts
2666
+ async function serveCommand(opts) {
2667
+ const cwd = process.cwd();
2668
+ const pqDir = resolve4(cwd, ".pipequery");
2669
+ const serverJsonPath = resolve4(pqDir, "server.json");
2670
+ if (opts.stop) {
2671
+ return stopDaemon(serverJsonPath);
2672
+ }
2673
+ const { config, path: configPath } = await loadConfig(cwd);
2674
+ const port = opts.port ?? config.server.port;
2675
+ const host = opts.host ?? config.server.host;
2676
+ if (opts.daemon) {
2677
+ return startDaemon(cwd, port, host);
2678
+ }
2679
+ const { app, sourceManager } = await createServer(config, configPath, cwd);
2680
+ await mkdir(pqDir, { recursive: true });
2681
+ const logPath = resolve4(pqDir, "server.log");
2682
+ await writeFile3(serverJsonPath, JSON.stringify({ url: `http://localhost:${port}`, pid: process.pid, daemon: false, log: logPath }));
2683
+ await app.listen({ port, host });
2684
+ const sourceCount = sourceManager.getSourceNames().length;
2685
+ const endpointCount = Object.keys(config.endpoints).length;
2686
+ const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
2687
+ printCompactBanner();
2688
+ printServerStartup({ url, sourceCount, endpointCount });
2689
+ const shutdown = async () => {
2690
+ log.dim("\nShutting down...");
2691
+ await sourceManager.dispose();
2692
+ await app.close();
2693
+ try {
2694
+ await unlink(serverJsonPath);
2695
+ } catch {
2696
+ }
2697
+ process.exit(0);
2698
+ };
2699
+ process.on("SIGINT", shutdown);
2700
+ process.on("SIGTERM", shutdown);
2701
+ }
2702
+ async function startDaemon(cwd, port, host) {
2703
+ const pqDir = resolve4(cwd, ".pipequery");
2704
+ const serverJsonPath = resolve4(pqDir, "server.json");
2705
+ const logPath = resolve4(pqDir, "server.log");
2706
+ if (existsSync3(serverJsonPath)) {
2707
+ try {
2708
+ const info2 = JSON.parse(await readFile3(serverJsonPath, "utf-8"));
2709
+ const res = await fetch(`${info2.url}/health`).catch(() => null);
2710
+ if (res?.ok) {
2711
+ log.warn(`Server already running at ${info2.url} (PID: ${info2.pid})`);
2712
+ return;
2713
+ }
2714
+ } catch {
2715
+ }
2716
+ }
2717
+ await mkdir(pqDir, { recursive: true });
2718
+ const scriptPath = process.argv[1];
2719
+ const args = ["serve", "--port", String(port), "--host", host];
2720
+ const out = await import("fs").then((fs) => fs.openSync(logPath, "a"));
2721
+ const child = spawn(process.execPath, [scriptPath, ...args], {
2722
+ cwd,
2723
+ detached: true,
2724
+ stdio: ["ignore", out, out],
2725
+ env: { ...process.env, PQ_DAEMON: "1" }
2726
+ });
2727
+ child.unref();
2728
+ await writeFile3(serverJsonPath, JSON.stringify({
2729
+ url: `http://localhost:${port}`,
2730
+ pid: child.pid,
2731
+ daemon: true,
2732
+ log: logPath
2733
+ }));
2734
+ log.success(`PipeQuery server started in background (PID: ${child.pid})`);
2735
+ log.dim(`Logs: ${logPath}`);
2736
+ log.dim(`Stop with: pq serve --stop`);
2737
+ }
2738
+ async function stopDaemon(serverJsonPath) {
2739
+ if (!existsSync3(serverJsonPath)) {
2740
+ log.error("No running server found.");
2741
+ return;
2742
+ }
2743
+ try {
2744
+ const info2 = JSON.parse(await readFile3(serverJsonPath, "utf-8"));
2745
+ process.kill(info2.pid, "SIGTERM");
2746
+ await unlink(serverJsonPath);
2747
+ log.success(`Server stopped (PID: ${info2.pid})`);
2748
+ } catch (err) {
2749
+ log.error(`Failed to stop server: ${err instanceof Error ? err.message : err}`);
2750
+ try {
2751
+ await unlink(serverJsonPath);
2752
+ } catch {
2753
+ }
2754
+ }
2755
+ }
2756
+
2757
+ // src/commands/query.ts
2758
+ import chalk4 from "chalk";
2759
+
2760
+ // src/utils/server-discovery.ts
2761
+ import { readFile as readFile4 } from "fs/promises";
2762
+ import { resolve as resolve5 } from "path";
2763
+ import { existsSync as existsSync4 } from "fs";
2764
+ async function getServerUrl() {
2765
+ const cwd = process.cwd();
2766
+ const serverJsonPath = resolve5(cwd, ".pipequery", "server.json");
2767
+ if (existsSync4(serverJsonPath)) {
2768
+ try {
2769
+ const info2 = JSON.parse(await readFile4(serverJsonPath, "utf-8"));
2770
+ const res = await fetch(`${info2.url}/health`).catch(() => null);
2771
+ if (res?.ok) return info2.url;
2772
+ } catch {
2773
+ }
2774
+ }
2775
+ const configPath = findConfigPath(cwd);
2776
+ if (configPath) {
2777
+ try {
2778
+ const config = await readYaml(configPath);
2779
+ if (config.remote?.url) {
2780
+ const res = await fetch(`${config.remote.url}/health`).catch(() => null);
2781
+ if (res?.ok) return config.remote.url;
2782
+ }
2783
+ } catch {
2784
+ }
2785
+ }
2786
+ throw new Error("No running PipeQuery server found. Start one with: pq serve");
2787
+ }
2788
+
2789
+ // src/commands/query.ts
2790
+ async function queryCommand(expression, opts) {
2791
+ const serverUrl = await getServerUrl();
2792
+ const res = await fetch(`${serverUrl}/api/_control/query`, {
2793
+ method: "POST",
2794
+ headers: { "Content-Type": "application/json" },
2795
+ body: JSON.stringify({ query: expression })
2796
+ });
2797
+ const json = await res.json();
2798
+ if (!json.ok) {
2799
+ log.error(json.error ?? "Query failed");
2800
+ process.exit(1);
2801
+ }
2802
+ const result = json.result;
2803
+ if (opts.format === "json") {
2804
+ console.log(JSON.stringify(result, null, 2));
2805
+ return;
2806
+ }
2807
+ if (Array.isArray(result) && result.length > 0 && typeof result[0] === "object") {
2808
+ renderTable(result);
2809
+ } else if (typeof result === "object" && result !== null && !Array.isArray(result)) {
2810
+ renderTable([result]);
2811
+ } else {
2812
+ console.log(result);
2813
+ }
2814
+ }
2815
+ function renderTable(rows) {
2816
+ if (rows.length === 0) {
2817
+ log.dim("(empty result)");
2818
+ return;
2819
+ }
2820
+ const columns = Object.keys(rows[0]);
2821
+ const widths = columns.map((col) => {
2822
+ const maxData = rows.reduce((max, row) => Math.max(max, formatCell(row[col]).length), 0);
2823
+ return Math.max(col.length, maxData);
2824
+ });
2825
+ const header = columns.map((col, i) => chalk4.hex("#06b6d4").bold(col.padEnd(widths[i]))).join(" ");
2826
+ const separator = widths.map((w) => chalk4.dim("\u2500".repeat(w))).join(chalk4.dim("\u2500\u2500"));
2827
+ console.log();
2828
+ console.log(` ${header}`);
2829
+ console.log(` ${separator}`);
2830
+ for (const row of rows) {
2831
+ const line = columns.map((col, i) => {
2832
+ const val = formatCell(row[col]);
2833
+ return typeof row[col] === "number" ? chalk4.hex("#f59e0b")(val.padStart(widths[i])) : val.padEnd(widths[i]);
2834
+ }).join(" ");
2835
+ console.log(` ${line}`);
2836
+ }
2837
+ console.log();
2838
+ console.log(chalk4.dim(` ${rows.length} row(s)`));
2839
+ console.log();
2840
+ }
2841
+ function formatCell(value) {
2842
+ if (value == null) return "";
2843
+ if (typeof value === "number") {
2844
+ return Number.isInteger(value) ? String(value) : value.toFixed(2);
2845
+ }
2846
+ return String(value);
2847
+ }
2848
+
2849
+ // src/commands/source.ts
2850
+ import chalk5 from "chalk";
2851
+ async function sourceListCommand() {
2852
+ const cwd = process.cwd();
2853
+ try {
2854
+ const serverUrl = await getServerUrl();
2855
+ const res = await fetch(`${serverUrl}/api/_control/sources`);
2856
+ const statuses = await res.json();
2857
+ if (Object.keys(statuses).length === 0) {
2858
+ log.dim("No sources configured. Add one with: pq source add <name>");
2859
+ return;
2860
+ }
2861
+ console.log(chalk5.bold("Sources:\n"));
2862
+ for (const [name, status] of Object.entries(statuses)) {
2863
+ const icon = status.healthy ? chalk5.green("\u25CF") : chalk5.red("\u25CF");
2864
+ const rows = chalk5.dim(`${status.rowCount} rows`);
2865
+ const err = status.error ? chalk5.red(` (${status.error})`) : "";
2866
+ console.log(` ${icon} ${chalk5.bold(name)} ${rows}${err}`);
2867
+ }
2868
+ } catch {
2869
+ const { config } = await loadConfig(cwd);
2870
+ const sources = Object.entries(config.sources);
2871
+ if (sources.length === 0) {
2872
+ log.dim("No sources configured. Add one with: pq source add <name>");
2873
+ return;
2874
+ }
2875
+ console.log(chalk5.bold("Sources (server not running):\n"));
2876
+ for (const [name, src] of sources) {
2877
+ const type = chalk5.cyan(src.type);
2878
+ const detail = "url" in src ? chalk5.dim(src.url) : "path" in src ? chalk5.dim(src.path) : "";
2879
+ console.log(` \u25CB ${chalk5.bold(name)} ${type} ${detail}`);
2880
+ }
2881
+ }
2882
+ }
2883
+ async function sourceAddCommand(name, opts) {
2884
+ const cwd = process.cwd();
2885
+ const { config, path: configPath } = await loadConfig(cwd);
2886
+ let sourceConfig;
2887
+ switch (opts.type) {
2888
+ case "rest":
2889
+ if (!opts.url) throw new Error("--url is required for REST sources");
2890
+ sourceConfig = {
2891
+ type: "rest",
2892
+ url: opts.url,
2893
+ interval: opts.interval ?? "30s",
2894
+ ...opts.dataPath ? { dataPath: opts.dataPath } : {}
2895
+ };
2896
+ break;
2897
+ case "websocket":
2898
+ if (!opts.url) throw new Error("--url is required for WebSocket sources");
2899
+ sourceConfig = { type: "websocket", url: opts.url };
2900
+ break;
2901
+ case "file":
2902
+ if (!opts.path) throw new Error("--path is required for file sources");
2903
+ sourceConfig = {
2904
+ type: "file",
2905
+ path: opts.path,
2906
+ ...opts.watch ? { watch: true } : {}
2907
+ };
2908
+ break;
2909
+ default:
2910
+ throw new Error(`Unknown source type: ${opts.type}. Use: rest, websocket, file`);
2911
+ }
2912
+ config.sources[name] = sourceConfig;
2913
+ await saveConfig(config, configPath);
2914
+ try {
2915
+ const serverUrl = await getServerUrl();
2916
+ const res = await fetch(`${serverUrl}/api/_control/sources`, {
2917
+ method: "POST",
2918
+ headers: { "Content-Type": "application/json" },
2919
+ body: JSON.stringify({ name, config: sourceConfig })
2920
+ });
2921
+ const json = await res.json();
2922
+ if (json.ok) {
2923
+ log.success(`Source "${name}" added and live (${json.status?.rowCount ?? 0} rows)`);
2924
+ return;
2925
+ }
2926
+ } catch {
2927
+ }
2928
+ log.success(`Source "${name}" added to config. Start server with: pq serve`);
2929
+ }
2930
+ async function sourceRemoveCommand(name) {
2931
+ const cwd = process.cwd();
2932
+ const { config, path: configPath } = await loadConfig(cwd);
2933
+ if (!config.sources[name]) {
2934
+ log.error(`Source "${name}" not found`);
2935
+ return;
2936
+ }
2937
+ delete config.sources[name];
2938
+ await saveConfig(config, configPath);
2939
+ try {
2940
+ const serverUrl = await getServerUrl();
2941
+ await fetch(`${serverUrl}/api/_control/sources/${encodeURIComponent(name)}`, { method: "DELETE" });
2942
+ log.success(`Source "${name}" removed (live)`);
2943
+ return;
2944
+ } catch {
2945
+ }
2946
+ log.success(`Source "${name}" removed from config`);
2947
+ }
2948
+ async function sourceTestCommand(name) {
2949
+ const serverUrl = await getServerUrl();
2950
+ const res = await fetch(`${serverUrl}/api/_control/sources/${encodeURIComponent(name)}/test`, { method: "POST" });
2951
+ const json = await res.json();
2952
+ if (!json.ok) {
2953
+ log.error(json.error ?? "Test failed");
2954
+ return;
2955
+ }
2956
+ log.success(`Source "${name}": ${json.rowCount} rows`);
2957
+ if (json.sample && json.sample.length > 0) {
2958
+ console.log(chalk5.dim("\nSample:"));
2959
+ console.log(JSON.stringify(json.sample, null, 2));
2960
+ }
2961
+ }
2962
+
2963
+ // src/commands/endpoint.ts
2964
+ import chalk6 from "chalk";
2965
+ async function endpointListCommand() {
2966
+ const cwd = process.cwd();
2967
+ try {
2968
+ const serverUrl = await getServerUrl();
2969
+ const res = await fetch(`${serverUrl}/api/_control/endpoints`);
2970
+ const endpoints = await res.json();
2971
+ if (Object.keys(endpoints).length === 0) {
2972
+ log.dim('No endpoints configured. Add one with: pq endpoint add <path> --query "<expr>"');
2973
+ return;
2974
+ }
2975
+ console.log(chalk6.bold("Endpoints:\n"));
2976
+ for (const [path, config] of Object.entries(endpoints)) {
2977
+ const cache = config.cache ? chalk6.dim(`(cache: ${config.cache})`) : "";
2978
+ console.log(` ${chalk6.green("GET")} ${chalk6.bold(path)} ${cache}`);
2979
+ console.log(` ${chalk6.dim(config.query)}`);
2980
+ }
2981
+ } catch {
2982
+ const { config } = await loadConfig(cwd);
2983
+ const entries2 = Object.entries(config.endpoints);
2984
+ if (entries2.length === 0) {
2985
+ log.dim('No endpoints configured. Add one with: pq endpoint add <path> --query "<expr>"');
2986
+ return;
2987
+ }
2988
+ console.log(chalk6.bold("Endpoints (server not running):\n"));
2989
+ for (const [path, cfg] of entries2) {
2990
+ console.log(` ${chalk6.green("GET")} ${chalk6.bold(path)}`);
2991
+ console.log(` ${chalk6.dim(cfg.query)}`);
2992
+ }
2993
+ }
2994
+ }
2995
+ async function endpointAddCommand(path, opts) {
2996
+ const cwd = process.cwd();
2997
+ const { config, path: configPath } = await loadConfig(cwd);
2998
+ const apiPath = path.startsWith("/") ? path : `/${path}`;
2999
+ const endpointConfig = {
3000
+ query: opts.query,
3001
+ ...opts.cache ? { cache: opts.cache } : {}
3002
+ };
3003
+ config.endpoints[apiPath] = endpointConfig;
3004
+ await saveConfig(config, configPath);
3005
+ try {
3006
+ const serverUrl = await getServerUrl();
3007
+ await fetch(`${serverUrl}/api/_control/endpoints`, {
3008
+ method: "POST",
3009
+ headers: { "Content-Type": "application/json" },
3010
+ body: JSON.stringify({ path: apiPath, config: endpointConfig })
3011
+ });
3012
+ log.success(`Endpoint ${apiPath} registered (live)`);
3013
+ return;
3014
+ } catch {
3015
+ }
3016
+ log.success(`Endpoint ${apiPath} added to config. Start server with: pq serve`);
3017
+ }
3018
+ async function endpointRemoveCommand(path) {
3019
+ const cwd = process.cwd();
3020
+ const { config, path: configPath } = await loadConfig(cwd);
3021
+ const apiPath = path.startsWith("/") ? path : `/${path}`;
3022
+ if (!config.endpoints[apiPath]) {
3023
+ log.error(`Endpoint "${apiPath}" not found`);
3024
+ return;
3025
+ }
3026
+ delete config.endpoints[apiPath];
3027
+ await saveConfig(config, configPath);
3028
+ try {
3029
+ const serverUrl = await getServerUrl();
3030
+ await fetch(`${serverUrl}/api/_control/endpoints/${apiPath.slice(1)}`, { method: "DELETE" });
3031
+ log.success(`Endpoint ${apiPath} removed (live)`);
3032
+ return;
3033
+ } catch {
3034
+ }
3035
+ log.success(`Endpoint ${apiPath} removed from config`);
3036
+ }
3037
+
3038
+ // src/commands/dashboard.ts
3039
+ import React4 from "react";
3040
+ import { render } from "ink";
3041
+
3042
+ // src/tui/App.tsx
3043
+ import React3, { useState as useState4, useMemo, useCallback as useCallback2 } from "react";
3044
+ import { Box as Box11, Text as Text10, useStdout } from "ink";
3045
+
3046
+ // src/tui/hooks/useLiveData.ts
3047
+ import { useState, useEffect, useCallback, useRef } from "react";
3048
+ import http from "http";
3049
+ function useLiveData(serverUrl, queries, fallbackMs) {
3050
+ const [results, setResults] = useState(/* @__PURE__ */ new Map());
3051
+ const [loading, setLoading] = useState(true);
3052
+ const [connected, setConnected] = useState(false);
3053
+ const abortRef = useRef(null);
3054
+ const fallbackTimerRef = useRef(null);
3055
+ const queriesKey = queries.join("|||");
3056
+ useEffect(() => {
3057
+ if (queries.length === 0) return;
3058
+ const abort = new AbortController();
3059
+ abortRef.current = abort;
3060
+ let sseConnected = false;
3061
+ const connectSSE = () => {
3062
+ if (abort.signal.aborted) return;
3063
+ const url = new URL("/api/_control/sse", serverUrl);
3064
+ url.searchParams.set("queries", JSON.stringify(queries));
3065
+ const req = http.get(url.toString(), { signal: abort.signal }, (res) => {
3066
+ if (res.statusCode !== 200) {
3067
+ startPolling();
3068
+ return;
3069
+ }
3070
+ sseConnected = true;
3071
+ setConnected(true);
3072
+ stopPolling();
3073
+ let buffer = "";
3074
+ res.on("data", (chunk) => {
3075
+ buffer += chunk.toString();
3076
+ const blocks = buffer.split("\n\n");
3077
+ buffer = blocks.pop() ?? "";
3078
+ const updates = [];
3079
+ for (const block of blocks) {
3080
+ const dataLine = block.split("\n").find((l) => l.startsWith("data: "));
3081
+ if (!dataLine) continue;
3082
+ try {
3083
+ const event = JSON.parse(dataLine.slice(6));
3084
+ updates.push({ index: event.panelIndex, result: event.result, error: event.error });
3085
+ } catch {
3086
+ }
3087
+ }
3088
+ if (updates.length > 0) {
3089
+ setResults((prev) => {
3090
+ const next = new Map(prev);
3091
+ for (const u of updates) {
3092
+ const query2 = queries[u.index];
3093
+ if (query2) {
3094
+ next.set(query2, u.error ? { data: null, error: u.error } : { data: u.result });
3095
+ }
3096
+ }
3097
+ return next;
3098
+ });
3099
+ setLoading(false);
3100
+ }
3101
+ });
3102
+ res.on("end", () => {
3103
+ sseConnected = false;
3104
+ setConnected(false);
3105
+ if (!abort.signal.aborted) {
3106
+ startPolling();
3107
+ setTimeout(connectSSE, 3e3);
3108
+ }
3109
+ });
3110
+ });
3111
+ req.on("error", () => {
3112
+ sseConnected = false;
3113
+ setConnected(false);
3114
+ if (!abort.signal.aborted) {
3115
+ startPolling();
3116
+ setTimeout(connectSSE, 3e3);
3117
+ }
3118
+ });
3119
+ };
3120
+ const pollAll = async () => {
3121
+ const entries2 = await Promise.all(
3122
+ queries.map(async (query2) => {
3123
+ try {
3124
+ const res = await fetch(`${serverUrl}/api/_control/query`, {
3125
+ method: "POST",
3126
+ headers: { "Content-Type": "application/json" },
3127
+ body: JSON.stringify({ query: query2 })
3128
+ });
3129
+ const json = await res.json();
3130
+ if (json.ok) return [query2, { data: json.result }];
3131
+ return [query2, { data: null, error: json.error }];
3132
+ } catch (err) {
3133
+ return [query2, { data: null, error: err instanceof Error ? err.message : String(err) }];
3134
+ }
3135
+ })
3136
+ );
3137
+ setResults(new Map(entries2));
3138
+ setLoading(false);
3139
+ };
3140
+ const startPolling = () => {
3141
+ if (fallbackTimerRef.current || sseConnected) return;
3142
+ pollAll();
3143
+ fallbackTimerRef.current = setInterval(pollAll, fallbackMs);
3144
+ };
3145
+ const stopPolling = () => {
3146
+ if (fallbackTimerRef.current) {
3147
+ clearInterval(fallbackTimerRef.current);
3148
+ fallbackTimerRef.current = null;
3149
+ }
3150
+ };
3151
+ connectSSE();
3152
+ return () => {
3153
+ abort.abort();
3154
+ stopPolling();
3155
+ };
3156
+ }, [serverUrl, queriesKey, fallbackMs]);
3157
+ const refresh = useCallback(async () => {
3158
+ try {
3159
+ await fetch(`${serverUrl}/api/_control/refresh`, { method: "POST" });
3160
+ } catch {
3161
+ }
3162
+ }, [serverUrl]);
3163
+ return { results, loading, connected, refresh };
3164
+ }
3165
+
3166
+ // src/tui/hooks/useKeyboard.ts
3167
+ import { useInput, useApp } from "ink";
3168
+ function useKeyboard({ panelCount, focusedPanel, setFocusedPanel, onRefresh, onResize }) {
3169
+ const { exit } = useApp();
3170
+ useInput((input, key) => {
3171
+ if (input === "q") exit();
3172
+ if (input === "r") onRefresh?.();
3173
+ if (key.tab && !key.shift) setFocusedPanel((i) => (i + 1) % panelCount);
3174
+ if (key.tab && key.shift) setFocusedPanel((i) => (i - 1 + panelCount) % panelCount);
3175
+ if (input === "]") onResize?.("grow");
3176
+ if (input === "[") onResize?.("shrink");
3177
+ });
3178
+ }
3179
+
3180
+ // src/tui/components/Panel.tsx
3181
+ import { Box, Text } from "ink";
3182
+ import { jsx, jsxs } from "react/jsx-runtime";
3183
+ function Panel({ title, focused = false, loading = false, error: error2, children }) {
3184
+ const borderColor = focused ? "cyan" : "gray";
3185
+ return /* @__PURE__ */ jsxs(
3186
+ Box,
3187
+ {
3188
+ flexDirection: "column",
3189
+ flexGrow: 1,
3190
+ borderStyle: "round",
3191
+ borderColor,
3192
+ paddingX: 1,
3193
+ overflow: "hidden",
3194
+ children: [
3195
+ /* @__PURE__ */ jsxs(Box, { children: [
3196
+ /* @__PURE__ */ jsx(Text, { bold: true, color: focused ? "cyan" : void 0, children: title }),
3197
+ loading && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u21BB" })
3198
+ ] }),
3199
+ error2 ? /* @__PURE__ */ jsx(Text, { color: "red", children: error2 }) : children
3200
+ ]
3201
+ }
3202
+ );
3203
+ }
3204
+
3205
+ // src/tui/components/Table.tsx
3206
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef2 } from "react";
3207
+ import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
3208
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
3209
+ function formatValue(val) {
3210
+ if (val == null) return "";
3211
+ if (typeof val === "number") return Number.isInteger(val) ? String(val) : val.toFixed(2);
3212
+ return String(val);
3213
+ }
3214
+ var SCROLLBAR_TRACK = "\u2502";
3215
+ var SCROLLBAR_THUMB = "\u2588";
3216
+ function Scrollbar({ totalRows, visibleRows, scrollOffset, height }) {
3217
+ if (totalRows <= visibleRows) return null;
3218
+ const thumbSize = Math.max(1, Math.round(visibleRows / totalRows * height));
3219
+ const maxScroll = totalRows - visibleRows;
3220
+ const thumbPosition = maxScroll > 0 ? Math.round(scrollOffset / maxScroll * (height - thumbSize)) : 0;
3221
+ const lines = [];
3222
+ for (let i = 0; i < height; i++) {
3223
+ const isThumb = i >= thumbPosition && i < thumbPosition + thumbSize;
3224
+ lines.push({ char: isThumb ? SCROLLBAR_THUMB : SCROLLBAR_TRACK, isThumb });
3225
+ }
3226
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginLeft: 1, children: lines.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: line.isThumb ? "#06b6d4" : "#333333", children: line.char }, i)) });
3227
+ }
3228
+ function Table({ data, maxRows = 20, focused = false }) {
3229
+ const [scrollOffset, setScrollOffset] = useState2(0);
3230
+ const prevDataLenRef = useRef2(data.length);
3231
+ useEffect2(() => {
3232
+ const maxOffset = Math.max(0, data.length - maxRows);
3233
+ setScrollOffset((prev) => Math.min(prev, maxOffset));
3234
+ prevDataLenRef.current = data.length;
3235
+ }, [data.length, maxRows]);
3236
+ useInput2((input, key) => {
3237
+ if (!focused) return;
3238
+ const maxOffset = Math.max(0, data.length - maxRows);
3239
+ if (key.upArrow) setScrollOffset((o) => Math.max(0, o - 1));
3240
+ if (key.downArrow) setScrollOffset((o) => Math.min(maxOffset, o + 1));
3241
+ if (key.pageUp || input === "u" && key.ctrl) {
3242
+ setScrollOffset((o) => Math.max(0, o - maxRows));
3243
+ }
3244
+ if (key.pageDown || input === "d" && key.ctrl) {
3245
+ setScrollOffset((o) => Math.min(maxOffset, o + maxRows));
3246
+ }
3247
+ });
3248
+ if (!data.length) return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "(empty)" });
3249
+ const columns = Object.keys(data[0]);
3250
+ const widths = columns.map((col) => {
3251
+ const maxData = data.reduce((max, row) => Math.max(max, formatValue(row[col]).length), 0);
3252
+ return Math.min(Math.max(col.length, maxData), 30);
3253
+ });
3254
+ const visibleRows = data.slice(scrollOffset, scrollOffset + maxRows);
3255
+ const needsScroll = data.length > maxRows;
3256
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
3257
+ /* @__PURE__ */ jsxs2(Box2, { children: [
3258
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, children: [
3259
+ /* @__PURE__ */ jsx2(Box2, { children: columns.map((col, i) => /* @__PURE__ */ jsx2(Box2, { width: widths[i] + 2, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: col.slice(0, widths[i]).padEnd(widths[i]) }) }, col)) }),
3260
+ /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: widths.map((w) => "\u2500".repeat(w)).join("\u2500\u2500") }) }),
3261
+ visibleRows.map((row, ri) => /* @__PURE__ */ jsx2(Box2, { children: columns.map((col, i) => /* @__PURE__ */ jsx2(Box2, { width: widths[i] + 2, children: /* @__PURE__ */ jsx2(Text2, { color: typeof row[col] === "number" ? "#f59e0b" : void 0, children: formatValue(row[col]).slice(0, widths[i]).padEnd(widths[i]) }) }, col)) }, ri))
3262
+ ] }),
3263
+ needsScroll && /* @__PURE__ */ jsx2(
3264
+ Scrollbar,
3265
+ {
3266
+ totalRows: data.length,
3267
+ visibleRows: maxRows,
3268
+ scrollOffset,
3269
+ height: visibleRows.length + 2
3270
+ }
3271
+ )
3272
+ ] }),
3273
+ needsScroll && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3274
+ scrollOffset + 1,
3275
+ "\u2013",
3276
+ Math.min(scrollOffset + maxRows, data.length),
3277
+ " of ",
3278
+ data.length,
3279
+ " rows",
3280
+ focused ? " \u2502 \u2191\u2193 scroll" : ""
3281
+ ] })
3282
+ ] });
3283
+ }
3284
+
3285
+ // src/tui/components/BarChart.tsx
3286
+ import { Box as Box3, Text as Text3 } from "ink";
3287
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3288
+ var BLOCK = "\u2588";
3289
+ var MAX_BAR_WIDTH = 40;
3290
+ function BarChart({ data, maxBars = 10 }) {
3291
+ if (!data.length) return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "(empty)" });
3292
+ const keys = Object.keys(data[0]);
3293
+ const labelKey = keys[0];
3294
+ const valueKey = keys.find((k) => typeof data[0][k] === "number") ?? keys[1];
3295
+ const items = data.slice(0, maxBars).map((row) => ({
3296
+ label: String(row[labelKey] ?? ""),
3297
+ value: Number(row[valueKey]) || 0
3298
+ }));
3299
+ const maxValue = Math.max(...items.map((i) => i.value), 1);
3300
+ const maxLabelLen = Math.max(...items.map((i) => i.label.length), 1);
3301
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: items.map((item, i) => {
3302
+ const barLen = Math.round(item.value / maxValue * MAX_BAR_WIDTH);
3303
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
3304
+ /* @__PURE__ */ jsxs3(Text3, { children: [
3305
+ item.label.padEnd(maxLabelLen),
3306
+ " "
3307
+ ] }),
3308
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: BLOCK.repeat(Math.max(1, barLen)) }),
3309
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3310
+ " ",
3311
+ formatNum(item.value)
3312
+ ] })
3313
+ ] }, i);
3314
+ }) });
3315
+ }
3316
+ function formatNum(n) {
3317
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
3318
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
3319
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
3320
+ return Number.isInteger(n) ? String(n) : n.toFixed(2);
3321
+ }
3322
+
3323
+ // src/tui/components/StatBox.tsx
3324
+ import { Box as Box4, Text as Text4 } from "ink";
3325
+ import { jsx as jsx4 } from "react/jsx-runtime";
3326
+ function StatBox({ data }) {
3327
+ let value;
3328
+ if (Array.isArray(data) && data.length === 1 && typeof data[0] === "object" && data[0] !== null) {
3329
+ const obj = data[0];
3330
+ const firstVal = Object.values(obj)[0];
3331
+ value = formatStat(firstVal);
3332
+ } else if (typeof data === "number" || typeof data === "string") {
3333
+ value = formatStat(data);
3334
+ } else {
3335
+ value = String(data);
3336
+ }
3337
+ return /* @__PURE__ */ jsx4(Box4, { justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "green", children: value }) });
3338
+ }
3339
+ function formatStat(val) {
3340
+ if (typeof val === "number") {
3341
+ if (val >= 1e12) return (val / 1e12).toFixed(2) + "T";
3342
+ if (val >= 1e9) return (val / 1e9).toFixed(2) + "B";
3343
+ if (val >= 1e6) return (val / 1e6).toFixed(2) + "M";
3344
+ if (val >= 1e3) return (val / 1e3).toFixed(2) + "K";
3345
+ return Number.isInteger(val) ? String(val) : val.toFixed(2);
3346
+ }
3347
+ return String(val ?? "");
3348
+ }
3349
+
3350
+ // src/tui/components/Sparkline.tsx
3351
+ import { Box as Box5, Text as Text5 } from "ink";
3352
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3353
+ var TICKS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
3354
+ function Sparkline({ data, maxPoints = 60 }) {
3355
+ if (!data.length) return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(empty)" });
3356
+ const keys = Object.keys(data[0]);
3357
+ const labelKey = keys[0];
3358
+ const valueKey = keys.find((k) => typeof data[0][k] === "number") ?? keys[1];
3359
+ if (!valueKey) return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "(no numeric column)" });
3360
+ const points = data.slice(-maxPoints);
3361
+ const values = points.map((row) => Number(row[valueKey]) || 0);
3362
+ const min = Math.min(...values);
3363
+ const max = Math.max(...values);
3364
+ const range = max - min || 1;
3365
+ const spark = values.map((v) => {
3366
+ const level = Math.round((v - min) / range * (TICKS.length - 1));
3367
+ return TICKS[Math.max(0, Math.min(TICKS.length - 1, level))];
3368
+ }).join("");
3369
+ const lastLabel = points.length > 0 ? String(points[points.length - 1][labelKey] ?? "") : "";
3370
+ const lastValue = values[values.length - 1];
3371
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
3372
+ /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: spark }) }),
3373
+ /* @__PURE__ */ jsxs4(Box5, { gap: 2, children: [
3374
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
3375
+ "min: ",
3376
+ formatNum2(min)
3377
+ ] }),
3378
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
3379
+ "max: ",
3380
+ formatNum2(max)
3381
+ ] }),
3382
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
3383
+ "last: ",
3384
+ formatNum2(lastValue)
3385
+ ] }),
3386
+ lastLabel && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
3387
+ "(",
3388
+ lastLabel,
3389
+ ")"
3390
+ ] })
3391
+ ] })
3392
+ ] });
3393
+ }
3394
+ function formatNum2(n) {
3395
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
3396
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
3397
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
3398
+ return Number.isInteger(n) ? String(n) : n.toFixed(2);
3399
+ }
3400
+
3401
+ // src/tui/components/OrderBook.tsx
3402
+ import { Box as Box6, Text as Text6 } from "ink";
3403
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3404
+ var BLOCK2 = "\u2588";
3405
+ var BAR_WIDTH = 20;
3406
+ var QTY_WIDTH = 10;
3407
+ var PRICE_WIDTH = 12;
3408
+ var ROWS = 10;
3409
+ function OrderBook({ data }) {
3410
+ const entries2 = Array.isArray(data) ? data : [];
3411
+ const latest = entries2[entries2.length - 1];
3412
+ if (!latest || !latest.bids || !latest.asks) {
3413
+ return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Waiting for order book data..." });
3414
+ }
3415
+ const bids = latest.bids.slice(0, ROWS).map(([p, q]) => ({
3416
+ price: parseFloat(p),
3417
+ qty: parseFloat(q)
3418
+ }));
3419
+ const asks = latest.asks.slice(0, ROWS).map(([p, q]) => ({
3420
+ price: parseFloat(p),
3421
+ qty: parseFloat(q)
3422
+ }));
3423
+ const bidCum = [];
3424
+ let sum = 0;
3425
+ for (const b of bids) {
3426
+ sum += b.qty;
3427
+ bidCum.push(sum);
3428
+ }
3429
+ const askCum = [];
3430
+ sum = 0;
3431
+ for (const a of asks) {
3432
+ sum += a.qty;
3433
+ askCum.push(sum);
3434
+ }
3435
+ const bidMax = bidCum[bidCum.length - 1] || 1;
3436
+ const askMax = askCum[askCum.length - 1] || 1;
3437
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
3438
+ /* @__PURE__ */ jsxs5(Box6, { children: [
3439
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: pad("BIDS", BAR_WIDTH, "right") }),
3440
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: pad("QTY", QTY_WIDTH, "left") }),
3441
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: pad("PRICE", PRICE_WIDTH, "left") }),
3442
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: pad("QTY", QTY_WIDTH, "left") }),
3443
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "ASKS" })
3444
+ ] }),
3445
+ Array.from({ length: ROWS }, (_, i) => {
3446
+ const bid = bids[i];
3447
+ const ask = asks[i];
3448
+ const bidWidth = bid ? Math.max(1, Math.round(bidCum[i] / bidMax * BAR_WIDTH)) : 0;
3449
+ const askWidth = ask ? Math.max(1, Math.round(askCum[i] / askMax * BAR_WIDTH)) : 0;
3450
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
3451
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: BLOCK2.repeat(bidWidth).padStart(BAR_WIDTH) }),
3452
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: pad(bid ? bid.qty.toFixed(4) : "", QTY_WIDTH, "left") }),
3453
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: i < bids.length ? "green" : "red", children: pad(formatPrice(bid?.price ?? ask?.price ?? 0), PRICE_WIDTH, "left") }),
3454
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: pad(ask ? ask.qty.toFixed(4) : "", QTY_WIDTH, "left") }),
3455
+ /* @__PURE__ */ jsx6(Text6, { color: "red", children: BLOCK2.repeat(askWidth).padEnd(BAR_WIDTH) })
3456
+ ] }, i);
3457
+ }),
3458
+ /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, children: [
3459
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Spread: " }),
3460
+ /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: asks.length > 0 && bids.length > 0 ? `${(asks[0].price - bids[0].price).toFixed(2)} (${((asks[0].price - bids[0].price) / bids[0].price * 100).toFixed(4)}%)` : "\u2014" })
3461
+ ] })
3462
+ ] });
3463
+ }
3464
+ function pad(s, width, align) {
3465
+ return align === "left" ? s.padStart(width) : s.padEnd(width);
3466
+ }
3467
+ function formatPrice(n) {
3468
+ if (n >= 1e3) return n.toFixed(2);
3469
+ if (n >= 1) return n.toFixed(4);
3470
+ return n.toFixed(6);
3471
+ }
3472
+
3473
+ // src/tui/components/Heatmap.tsx
3474
+ import { Box as Box7, Text as Text7 } from "ink";
3475
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3476
+ function Heatmap({ data }) {
3477
+ if (!data.length) return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(empty)" });
3478
+ const keys = Object.keys(data[0]);
3479
+ const labelKey = keys[0];
3480
+ const numericKeys = keys.filter((k) => {
3481
+ const v = data[0][k];
3482
+ return typeof v === "number" || typeof v === "string" && v !== "" && !isNaN(Number(v));
3483
+ });
3484
+ if (numericKeys.length === 0) return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No numeric columns for heatmap" });
3485
+ const ranges = /* @__PURE__ */ new Map();
3486
+ for (const key of numericKeys) {
3487
+ const vals = data.map((r) => Number(r[key]) || 0);
3488
+ ranges.set(key, { min: Math.min(...vals), max: Math.max(...vals) });
3489
+ }
3490
+ const maxLabel = Math.max(...data.map((r) => String(r[labelKey] ?? "").length), 6);
3491
+ const colWidth = 10;
3492
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
3493
+ /* @__PURE__ */ jsxs6(Box7, { children: [
3494
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " ".repeat(maxLabel + 1) }),
3495
+ numericKeys.map((k) => /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: k.slice(0, colWidth).padStart(colWidth) }, k))
3496
+ ] }),
3497
+ data.slice(0, 20).map((row, i) => /* @__PURE__ */ jsxs6(Box7, { children: [
3498
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: String(row[labelKey] ?? "").slice(0, maxLabel).padEnd(maxLabel) }),
3499
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
3500
+ numericKeys.map((k) => {
3501
+ const val = Number(row[k]) || 0;
3502
+ const range = ranges.get(k);
3503
+ const norm = range.max === range.min ? 0.5 : (val - range.min) / (range.max - range.min);
3504
+ const color = interpolateColor(norm);
3505
+ const display = formatCell2(val);
3506
+ return /* @__PURE__ */ jsx7(Text7, { backgroundColor: color, color: "white", children: display.padStart(colWidth) }, k);
3507
+ })
3508
+ ] }, i))
3509
+ ] });
3510
+ }
3511
+ function interpolateColor(t) {
3512
+ let r, g, b;
3513
+ if (t < 0.5) {
3514
+ const s = t * 2;
3515
+ r = 180;
3516
+ g = Math.round(s * 180);
3517
+ b = 30;
3518
+ } else {
3519
+ const s = (t - 0.5) * 2;
3520
+ r = Math.round(180 * (1 - s));
3521
+ g = 180;
3522
+ b = 30;
3523
+ }
3524
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
3525
+ }
3526
+ function formatCell2(n) {
3527
+ if (Math.abs(n) >= 1e9) return (n / 1e9).toFixed(1) + "B";
3528
+ if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(1) + "M";
3529
+ if (Math.abs(n) >= 1e3) return (n / 1e3).toFixed(1) + "K";
3530
+ if (Number.isInteger(n)) return String(n);
3531
+ return n.toFixed(2);
3532
+ }
3533
+
3534
+ // src/tui/components/CandleChart.tsx
3535
+ import { Box as Box8, Text as Text8 } from "ink";
3536
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3537
+ var CHART_HEIGHT = 14;
3538
+ function CandleChart({ data }) {
3539
+ if (!data.length) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Waiting for candle data..." });
3540
+ const candles = data.map((item) => {
3541
+ const k = item.k;
3542
+ if (!k) return null;
3543
+ return {
3544
+ open: parseFloat(k.o),
3545
+ high: parseFloat(k.h),
3546
+ low: parseFloat(k.l),
3547
+ close: parseFloat(k.c),
3548
+ time: k.t
3549
+ };
3550
+ }).filter((c) => c !== null);
3551
+ const byTime = /* @__PURE__ */ new Map();
3552
+ for (const c of candles) byTime.set(c.time, c);
3553
+ const unique = [...byTime.values()].slice(-20);
3554
+ if (unique.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Waiting for candle data..." });
3555
+ const allHigh = Math.max(...unique.map((c) => c.high));
3556
+ const allLow = Math.min(...unique.map((c) => c.low));
3557
+ const priceRange = allHigh - allLow || 1;
3558
+ const toRow = (price) => Math.min(CHART_HEIGHT - 1, Math.max(0, Math.round((allHigh - price) / priceRange * (CHART_HEIGHT - 1))));
3559
+ const priceLabels = Array.from({ length: CHART_HEIGHT }, (_, row) => {
3560
+ const price = allHigh - row / (CHART_HEIGHT - 1) * priceRange;
3561
+ return formatPrice2(price);
3562
+ });
3563
+ const labelWidth = Math.max(...priceLabels.map((l) => l.length));
3564
+ const rows = [];
3565
+ for (let row = 0; row < CHART_HEIGHT; row++) {
3566
+ const cells = [];
3567
+ for (let col = 0; col < unique.length; col++) {
3568
+ const c = unique[col];
3569
+ const bullish = c.close >= c.open;
3570
+ const color = bullish ? "green" : "red";
3571
+ const bodyTop = toRow(Math.max(c.open, c.close));
3572
+ const bodyBot = toRow(Math.min(c.open, c.close));
3573
+ const wickTop = toRow(c.high);
3574
+ const wickBot = toRow(c.low);
3575
+ let char;
3576
+ let cellColor;
3577
+ if (row >= bodyTop && row <= bodyBot) {
3578
+ char = "\u2588\u2588\u2588";
3579
+ cellColor = color;
3580
+ } else if (row >= wickTop && row <= wickBot) {
3581
+ char = " \u2502 ";
3582
+ cellColor = color;
3583
+ } else {
3584
+ char = " ";
3585
+ cellColor = "white";
3586
+ }
3587
+ cells.push(
3588
+ /* @__PURE__ */ jsx8(Text8, { color: cellColor, children: char }, col)
3589
+ );
3590
+ }
3591
+ rows.push(
3592
+ /* @__PURE__ */ jsxs7(Box8, { children: [
3593
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
3594
+ priceLabels[row].padStart(labelWidth),
3595
+ " "
3596
+ ] }),
3597
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u2502" }),
3598
+ cells
3599
+ ] }, row)
3600
+ );
3601
+ }
3602
+ const latest = unique[unique.length - 1];
3603
+ const oldest = unique[0];
3604
+ const fmtTime = (t) => new Date(t).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
3605
+ const axisWidth = unique.length * 3;
3606
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
3607
+ rows,
3608
+ /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
3609
+ " ".repeat(labelWidth + 1),
3610
+ "\u2514",
3611
+ "\u2500".repeat(axisWidth)
3612
+ ] }) }),
3613
+ /* @__PURE__ */ jsx8(Box8, { children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
3614
+ " ".repeat(labelWidth + 2),
3615
+ fmtTime(oldest.time).padEnd(axisWidth - fmtTime(latest.time).length),
3616
+ fmtTime(latest.time)
3617
+ ] }) })
3618
+ ] });
3619
+ }
3620
+ function formatPrice2(n) {
3621
+ if (n >= 1e3) return n.toFixed(0);
3622
+ if (n >= 1) return n.toFixed(2);
3623
+ return n.toFixed(4);
3624
+ }
3625
+
3626
+ // src/tui/components/Layout.tsx
3627
+ import { Box as Box9 } from "ink";
3628
+ import { jsx as jsx9 } from "react/jsx-runtime";
3629
+ function Layout({ panels, focusedIndex, colWeights, rowHeight, renderPanel }) {
3630
+ const rows = [];
3631
+ for (let i = 0; i < panels.length; i += 2) {
3632
+ const row = [{ panel: panels[i], index: i }];
3633
+ if (i + 1 < panels.length) {
3634
+ row.push({ panel: panels[i + 1], index: i + 1 });
3635
+ }
3636
+ rows.push(row);
3637
+ }
3638
+ return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexGrow: 1, children: rows.map((row, rowIdx) => /* @__PURE__ */ jsx9(Box9, { flexDirection: "row", height: rowHeight, children: row.map(({ panel, index }) => {
3639
+ const width = row.length === 1 ? "100%" : `${colWeights[index]}%`;
3640
+ return /* @__PURE__ */ jsx9(Box9, { width, minWidth: 20, children: renderPanel(panel, index, index === focusedIndex) }, index);
3641
+ }) }, rowIdx)) });
3642
+ }
3643
+
3644
+ // src/tui/components/StatusBar.tsx
3645
+ import { useState as useState3, useEffect as useEffect3 } from "react";
3646
+ import { Box as Box10, Text as Text9 } from "ink";
3647
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
3648
+ function StatusBar({ serverUrl, refreshMs }) {
3649
+ const [status, setStatus] = useState3(null);
3650
+ useEffect3(() => {
3651
+ const fetchStatus = async () => {
3652
+ try {
3653
+ const res = await fetch(`${serverUrl}/status`);
3654
+ const data = await res.json();
3655
+ setStatus({ uptime: data.uptime, sources: Object.keys(data.sources).length });
3656
+ } catch {
3657
+ setStatus(null);
3658
+ }
3659
+ };
3660
+ fetchStatus();
3661
+ const timer = setInterval(fetchStatus, refreshMs);
3662
+ return () => clearInterval(timer);
3663
+ }, [serverUrl, refreshMs]);
3664
+ const uptimeStr = status ? formatUptime(status.uptime) : "\u2014";
3665
+ const sourcesStr = status ? `${status.sources} source(s)` : "disconnected";
3666
+ const refreshStr = `${Math.floor(refreshMs / 1e3)}s`;
3667
+ return /* @__PURE__ */ jsxs8(Box10, { justifyContent: "space-between", paddingX: 1, borderStyle: "single", borderColor: "gray", borderTop: false, borderLeft: false, borderRight: false, children: [
3668
+ /* @__PURE__ */ jsxs8(Box10, { gap: 2, children: [
3669
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
3670
+ "server: ",
3671
+ serverUrl
3672
+ ] }),
3673
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
3674
+ "uptime: ",
3675
+ uptimeStr
3676
+ ] }),
3677
+ /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: sourcesStr })
3678
+ ] }),
3679
+ /* @__PURE__ */ jsxs8(Box10, { gap: 2, children: [
3680
+ /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
3681
+ "refresh: ",
3682
+ refreshStr
3683
+ ] }),
3684
+ /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "q: quit tab: focus \u2191\u2193: scroll []: resize r: refresh" })
3685
+ ] })
3686
+ ] });
3687
+ }
3688
+ function formatUptime(ms) {
3689
+ const s = Math.floor(ms / 1e3);
3690
+ if (s < 60) return `${s}s`;
3691
+ if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
3692
+ return `${Math.floor(s / 3600)}h ${Math.floor(s % 3600 / 60)}m`;
3693
+ }
3694
+
3695
+ // src/tui/App.tsx
3696
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
3697
+ var MemoizedPanelContent = React3.memo(function PanelContent({ data, error: error2, viz, focused, loading, panelKey, maxTableRows }) {
3698
+ if (error2) return /* @__PURE__ */ jsx11(Text10, { color: "red", children: error2 });
3699
+ if (loading || data == null) return /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "Loading..." });
3700
+ const rows = Array.isArray(data) ? data : [];
3701
+ switch (viz) {
3702
+ case "bar":
3703
+ return /* @__PURE__ */ jsx11(BarChart, { data: rows });
3704
+ case "stat":
3705
+ return /* @__PURE__ */ jsx11(StatBox, { data });
3706
+ case "sparkline":
3707
+ return /* @__PURE__ */ jsx11(Sparkline, { data: rows });
3708
+ case "orderbook":
3709
+ return /* @__PURE__ */ jsx11(OrderBook, { data });
3710
+ case "heatmap":
3711
+ return /* @__PURE__ */ jsx11(Heatmap, { data: rows });
3712
+ case "candle":
3713
+ return /* @__PURE__ */ jsx11(CandleChart, { data: Array.isArray(data) ? data : [] });
3714
+ case "table":
3715
+ return /* @__PURE__ */ jsx11(Table, { data: rows, focused, maxRows: maxTableRows });
3716
+ case "auto":
3717
+ default:
3718
+ if (!Array.isArray(data) || rows.length === 0) return /* @__PURE__ */ jsx11(StatBox, { data });
3719
+ return /* @__PURE__ */ jsx11(Table, { data: rows, focused, maxRows: maxTableRows });
3720
+ }
3721
+ });
3722
+ function App({ serverUrl, dashboard }) {
3723
+ const [focusedPanel, setFocusedPanel] = useState4(0);
3724
+ const { stdout } = useStdout();
3725
+ const termHeight = stdout?.rows ?? 40;
3726
+ const panels = dashboard.panels;
3727
+ const refreshMs = dashboard.refresh ? parseDuration(dashboard.refresh) : 1e4;
3728
+ const [colWeights, setColWeights] = useState4(
3729
+ () => panels.map(() => 50)
3730
+ );
3731
+ const fixedOverhead = 3;
3732
+ const numPanelRows = Math.ceil(panels.length / 2);
3733
+ const rowHeight = Math.max(8, Math.floor((termHeight - fixedOverhead) / numPanelRows));
3734
+ const panelOverhead = 5;
3735
+ const maxTableRows = Math.max(3, rowHeight - panelOverhead);
3736
+ const queries = useMemo(() => panels.map((p) => p.query), [panels]);
3737
+ const { results, loading, connected, refresh } = useLiveData(serverUrl, queries, refreshMs);
3738
+ const handleResize = useCallback2((direction) => {
3739
+ setColWeights((prev) => {
3740
+ const next = [...prev];
3741
+ const rowStart = Math.floor(focusedPanel / 2) * 2;
3742
+ const partner = focusedPanel % 2 === 0 ? focusedPanel + 1 : focusedPanel - 1;
3743
+ if (partner < 0 || partner >= panels.length) return prev;
3744
+ if (partner < rowStart || partner >= rowStart + 2) return prev;
3745
+ const delta = direction === "grow" ? 5 : -5;
3746
+ const newVal = Math.min(75, Math.max(25, next[focusedPanel] + delta));
3747
+ next[focusedPanel] = newVal;
3748
+ next[partner] = 100 - newVal;
3749
+ return next;
3750
+ });
3751
+ }, [focusedPanel, panels.length]);
3752
+ useKeyboard({
3753
+ panelCount: panels.length,
3754
+ focusedPanel,
3755
+ setFocusedPanel,
3756
+ onRefresh: refresh,
3757
+ onResize: handleResize
3758
+ });
3759
+ const renderPanel = useCallback2((panel, index, focused) => {
3760
+ const result = results.get(panel.query);
3761
+ return /* @__PURE__ */ jsx11(Panel, { title: panel.title, focused, children: /* @__PURE__ */ jsx11(
3762
+ MemoizedPanelContent,
3763
+ {
3764
+ panelKey: `panel-${index}`,
3765
+ data: result?.data ?? null,
3766
+ error: result?.error,
3767
+ viz: panel.viz,
3768
+ focused,
3769
+ loading: loading && !result,
3770
+ maxTableRows
3771
+ }
3772
+ ) });
3773
+ }, [results, loading, maxTableRows]);
3774
+ return /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", height: termHeight, children: [
3775
+ /* @__PURE__ */ jsxs9(Box11, { justifyContent: "center", paddingX: 1, children: [
3776
+ /* @__PURE__ */ jsx11(Text10, { bold: true, color: "cyan", children: "PipeQuery Dashboard" }),
3777
+ connected && /* @__PURE__ */ jsx11(Text10, { color: "green", children: " \u25CF LIVE" }),
3778
+ !connected && !loading && /* @__PURE__ */ jsx11(Text10, { color: "yellow", children: " \u25CB polling" })
3779
+ ] }),
3780
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: /* @__PURE__ */ jsx11(
3781
+ Layout,
3782
+ {
3783
+ panels,
3784
+ focusedIndex: focusedPanel,
3785
+ colWeights,
3786
+ rowHeight,
3787
+ renderPanel
3788
+ }
3789
+ ) }),
3790
+ /* @__PURE__ */ jsx11(StatusBar, { serverUrl, refreshMs })
3791
+ ] });
3792
+ }
3793
+
3794
+ // src/utils/sync-output.ts
3795
+ var BEGIN_SYNC = "\x1B[?2026h";
3796
+ var END_SYNC = "\x1B[?2026l";
3797
+ var patched = false;
3798
+ var pendingTimer = null;
3799
+ var inSync = false;
3800
+ function enableSyncOutput() {
3801
+ if (patched) return;
3802
+ patched = true;
3803
+ const originalWrite = process.stdout.write.bind(process.stdout);
3804
+ process.stdout.write = function(chunk, encodingOrCallback, callback) {
3805
+ if (!inSync) {
3806
+ inSync = true;
3807
+ originalWrite(BEGIN_SYNC);
3808
+ }
3809
+ if (pendingTimer) {
3810
+ clearTimeout(pendingTimer);
3811
+ }
3812
+ pendingTimer = setTimeout(() => {
3813
+ originalWrite(END_SYNC);
3814
+ inSync = false;
3815
+ pendingTimer = null;
3816
+ }, 0);
3817
+ return originalWrite(chunk, encodingOrCallback, callback);
3818
+ };
3819
+ }
3820
+ function disableSyncOutput() {
3821
+ if (inSync) {
3822
+ process.stdout.write("\x1B[?2026l");
3823
+ inSync = false;
3824
+ }
3825
+ if (pendingTimer) {
3826
+ clearTimeout(pendingTimer);
3827
+ pendingTimer = null;
3828
+ }
3829
+ }
3830
+
3831
+ // src/commands/dashboard.ts
3832
+ async function dashboardCommand(opts) {
3833
+ const dashboardName = opts.name ?? "main";
3834
+ const cwd = process.cwd();
3835
+ const { config } = await loadConfig(cwd);
3836
+ const dashboard = config.dashboards[dashboardName];
3837
+ if (!dashboard) {
3838
+ log.error(`Dashboard "${dashboardName}" not found in config.`);
3839
+ const available = Object.keys(config.dashboards);
3840
+ if (available.length > 0) {
3841
+ log.info(`Available dashboards: ${available.join(", ")}`);
3842
+ } else {
3843
+ log.dim("Add a dashboard section to pipequery.yaml");
3844
+ }
3845
+ return;
3846
+ }
3847
+ const serverUrl = await getServerUrl();
3848
+ process.stdout.write("\x1B[?1049h");
3849
+ process.stdout.write("\x1B[?25l");
3850
+ enableSyncOutput();
3851
+ const { waitUntilExit } = render(
3852
+ React4.createElement(App, { serverUrl, dashboard }),
3853
+ { patchConsole: false }
3854
+ );
3855
+ await waitUntilExit();
3856
+ disableSyncOutput();
3857
+ process.stdout.write("\x1B[?25h");
3858
+ process.stdout.write("\x1B[?1049l");
3859
+ }
3860
+
3861
+ // src/commands/monitor.ts
3862
+ import React6 from "react";
3863
+ import { render as render2 } from "ink";
3864
+ import { existsSync as existsSync5, readFileSync } from "fs";
3865
+ import { resolve as resolve6 } from "path";
3866
+
3867
+ // src/tui/MonitorApp.tsx
3868
+ import { useState as useState5, useEffect as useEffect4 } from "react";
3869
+ import { Box as Box12, Text as Text11, useApp as useApp2, useInput as useInput3 } from "ink";
3870
+ import { Fragment, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3871
+ function useServerStatus(serverUrl, refreshMs) {
3872
+ const [info2, setInfo] = useState5(null);
3873
+ const [connected, setConnected] = useState5(false);
3874
+ useEffect4(() => {
3875
+ const fetchStatus = async () => {
3876
+ try {
3877
+ const res = await fetch(`${serverUrl}/status`);
3878
+ const data = await res.json();
3879
+ setInfo(data);
3880
+ setConnected(true);
3881
+ } catch {
3882
+ setInfo(null);
3883
+ setConnected(false);
3884
+ }
3885
+ };
3886
+ fetchStatus();
3887
+ const timer = setInterval(fetchStatus, refreshMs);
3888
+ return () => clearInterval(timer);
3889
+ }, [serverUrl, refreshMs]);
3890
+ return { info: info2, connected };
3891
+ }
3892
+ function useEndpoints(serverUrl, refreshMs) {
3893
+ const [endpoints, setEndpoints] = useState5({});
3894
+ useEffect4(() => {
3895
+ const fetchEndpoints = async () => {
3896
+ try {
3897
+ const res = await fetch(`${serverUrl}/api/_control/endpoints`);
3898
+ const data = await res.json();
3899
+ setEndpoints(data);
3900
+ } catch {
3901
+ }
3902
+ };
3903
+ fetchEndpoints();
3904
+ const timer = setInterval(fetchEndpoints, refreshMs);
3905
+ return () => clearInterval(timer);
3906
+ }, [serverUrl, refreshMs]);
3907
+ return endpoints;
3908
+ }
3909
+ function useActivityLog(serverUrl, refreshMs, maxLines) {
3910
+ const [entries2, setEntries] = useState5([]);
3911
+ useEffect4(() => {
3912
+ const fetchActivity = async () => {
3913
+ try {
3914
+ const res = await fetch(`${serverUrl}/api/_control/activity`);
3915
+ const data = await res.json();
3916
+ setEntries(data.slice(-maxLines));
3917
+ } catch {
3918
+ }
3919
+ };
3920
+ fetchActivity();
3921
+ const timer = setInterval(fetchActivity, refreshMs);
3922
+ return () => clearInterval(timer);
3923
+ }, [serverUrl, refreshMs, maxLines]);
3924
+ return entries2;
3925
+ }
3926
+ function Header({ connected, uptime }) {
3927
+ return /* @__PURE__ */ jsxs10(Box12, { paddingX: 1, justifyContent: "space-between", children: [
3928
+ /* @__PURE__ */ jsxs10(Box12, { gap: 1, children: [
3929
+ /* @__PURE__ */ jsx12(Text11, { bold: true, color: "#06b6d4", children: "\u2B21" }),
3930
+ /* @__PURE__ */ jsx12(Text11, { bold: true, color: "#06b6d4", children: "PipeQuery Monitor" })
3931
+ ] }),
3932
+ /* @__PURE__ */ jsxs10(Box12, { gap: 2, children: [
3933
+ /* @__PURE__ */ jsx12(Text11, { color: connected ? "#22c55e" : "#ef4444", children: connected ? "\u25CF connected" : "\u25CF disconnected" }),
3934
+ /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
3935
+ "uptime: ",
3936
+ formatUptime2(uptime)
3937
+ ] })
3938
+ ] })
3939
+ ] });
3940
+ }
3941
+ function SourcesPanel({ sources }) {
3942
+ const entries2 = Object.entries(sources);
3943
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "#4f46e5", paddingX: 1, children: [
3944
+ /* @__PURE__ */ jsx12(Box12, { marginBottom: 0, children: /* @__PURE__ */ jsxs10(Text11, { bold: true, color: "#4f46e5", children: [
3945
+ " Sources (",
3946
+ entries2.length,
3947
+ ") "
3948
+ ] }) }),
3949
+ entries2.length === 0 ? /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: " No sources configured" }) : /* @__PURE__ */ jsxs10(Fragment, { children: [
3950
+ /* @__PURE__ */ jsxs10(Box12, { children: [
3951
+ /* @__PURE__ */ jsx12(Box12, { width: 20, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "NAME" }) }),
3952
+ /* @__PURE__ */ jsx12(Box12, { width: 10, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "STATUS" }) }),
3953
+ /* @__PURE__ */ jsx12(Box12, { width: 10, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "ROWS" }) }),
3954
+ /* @__PURE__ */ jsx12(Box12, { width: 22, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "LAST FETCH" }) }),
3955
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "ERROR" }) })
3956
+ ] }),
3957
+ entries2.map(([name, status]) => /* @__PURE__ */ jsxs10(Box12, { children: [
3958
+ /* @__PURE__ */ jsx12(Box12, { width: 20, children: /* @__PURE__ */ jsx12(Text11, { color: "#06b6d4", children: name }) }),
3959
+ /* @__PURE__ */ jsx12(Box12, { width: 10, children: /* @__PURE__ */ jsx12(Text11, { color: status.healthy ? "#22c55e" : "#ef4444", children: status.healthy ? "\u2713 ok" : "\u2717 err" }) }),
3960
+ /* @__PURE__ */ jsx12(Box12, { width: 10, children: /* @__PURE__ */ jsx12(Text11, { children: String(status.rowCount) }) }),
3961
+ /* @__PURE__ */ jsx12(Box12, { width: 22, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: status.lastFetch ? new Date(status.lastFetch).toLocaleTimeString() : "\u2014" }) }),
3962
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text11, { color: "#ef4444", children: status.error ?? "" }) })
3963
+ ] }, name))
3964
+ ] })
3965
+ ] });
3966
+ }
3967
+ function EndpointsPanel({ endpoints, serverUrl }) {
3968
+ const entries2 = Object.entries(endpoints);
3969
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "#7c3aed", paddingX: 1, children: [
3970
+ /* @__PURE__ */ jsx12(Box12, { marginBottom: 0, children: /* @__PURE__ */ jsxs10(Text11, { bold: true, color: "#7c3aed", children: [
3971
+ " Endpoints (",
3972
+ entries2.length,
3973
+ ") "
3974
+ ] }) }),
3975
+ entries2.length === 0 ? /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: " No endpoints registered" }) : /* @__PURE__ */ jsxs10(Fragment, { children: [
3976
+ /* @__PURE__ */ jsxs10(Box12, { children: [
3977
+ /* @__PURE__ */ jsx12(Box12, { width: 30, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "PATH" }) }),
3978
+ /* @__PURE__ */ jsx12(Box12, { width: 10, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "CACHE" }) }),
3979
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, bold: true, children: "QUERY" }) })
3980
+ ] }),
3981
+ entries2.map(([path, config]) => /* @__PURE__ */ jsxs10(Box12, { children: [
3982
+ /* @__PURE__ */ jsx12(Box12, { width: 30, children: /* @__PURE__ */ jsxs10(Text11, { color: "#06b6d4", children: [
3983
+ serverUrl,
3984
+ path
3985
+ ] }) }),
3986
+ /* @__PURE__ */ jsx12(Box12, { width: 10, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: config.cache ?? "\u2014" }) }),
3987
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text11, { children: config.query }) })
3988
+ ] }, path))
3989
+ ] })
3990
+ ] });
3991
+ }
3992
+ function ActivityPanel({ entries: entries2 }) {
3993
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
3994
+ /* @__PURE__ */ jsx12(Box12, { marginBottom: 0, children: /* @__PURE__ */ jsx12(Text11, { bold: true, color: "gray", children: " Activity Log " }) }),
3995
+ entries2.length === 0 ? /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: " No activity yet \u2014 run queries, add sources, or hit endpoints" }) : entries2.map((entry, i) => {
3996
+ const time = new Date(entry.time).toLocaleTimeString();
3997
+ const levelColor = entry.level === "error" ? "#ef4444" : entry.level === "warn" ? "#f59e0b" : "#22c55e";
3998
+ const levelIcon = entry.level === "error" ? "\u2717" : entry.level === "warn" ? "\u26A0" : "\u2192";
3999
+ return /* @__PURE__ */ jsxs10(Box12, { gap: 1, children: [
4000
+ /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: time }),
4001
+ /* @__PURE__ */ jsx12(Text11, { color: levelColor, children: levelIcon }),
4002
+ /* @__PURE__ */ jsx12(Text11, { color: entry.level === "error" ? "#ef4444" : void 0, children: entry.message })
4003
+ ] }, i);
4004
+ })
4005
+ ] });
4006
+ }
4007
+ function HelpBar() {
4008
+ return /* @__PURE__ */ jsxs10(Box12, { paddingX: 1, justifyContent: "center", gap: 3, children: [
4009
+ /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "q: quit" }),
4010
+ /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "r: refresh" }),
4011
+ /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "auto-refresh: 2s" })
4012
+ ] });
4013
+ }
4014
+ function formatUptime2(ms) {
4015
+ const s = Math.floor(ms / 1e3);
4016
+ if (s < 60) return `${s}s`;
4017
+ if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
4018
+ const h = Math.floor(s / 3600);
4019
+ const m = Math.floor(s % 3600 / 60);
4020
+ return `${h}h ${m}m`;
4021
+ }
4022
+ function MonitorApp({ serverUrl }) {
4023
+ const { exit } = useApp2();
4024
+ const { info: info2, connected } = useServerStatus(serverUrl, 2e3);
4025
+ const endpoints = useEndpoints(serverUrl, 5e3);
4026
+ const activityEntries = useActivityLog(serverUrl, 2e3, 15);
4027
+ const [, setTick] = useState5(0);
4028
+ useInput3((input) => {
4029
+ if (input === "q") exit();
4030
+ if (input === "r") setTick((t) => t + 1);
4031
+ });
4032
+ const sources = info2?.sources ?? {};
4033
+ const uptime = info2?.uptime ?? 0;
4034
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", padding: 0, children: [
4035
+ /* @__PURE__ */ jsx12(Header, { connected, uptime }),
4036
+ /* @__PURE__ */ jsx12(Box12, { height: 1 }),
4037
+ /* @__PURE__ */ jsx12(SourcesPanel, { sources }),
4038
+ /* @__PURE__ */ jsx12(Box12, { height: 1 }),
4039
+ /* @__PURE__ */ jsx12(EndpointsPanel, { endpoints, serverUrl }),
4040
+ /* @__PURE__ */ jsx12(Box12, { height: 1 }),
4041
+ /* @__PURE__ */ jsx12(ActivityPanel, { entries: activityEntries }),
4042
+ /* @__PURE__ */ jsx12(HelpBar, {})
4043
+ ] });
4044
+ }
4045
+
4046
+ // src/commands/monitor.ts
4047
+ async function monitorCommand() {
4048
+ const serverUrl = await getServerUrl();
4049
+ const discoveryPath = resolve6(process.cwd(), ".pipequery", "server.json");
4050
+ let logPath;
4051
+ if (existsSync5(discoveryPath)) {
4052
+ try {
4053
+ const disc = JSON.parse(readFileSync(discoveryPath, "utf-8"));
4054
+ if (disc.log && existsSync5(disc.log)) {
4055
+ logPath = disc.log;
4056
+ }
4057
+ } catch {
4058
+ }
4059
+ }
4060
+ if (!logPath) {
4061
+ const fallback = resolve6(process.cwd(), ".pipequery", "server.log");
4062
+ if (existsSync5(fallback)) {
4063
+ logPath = fallback;
4064
+ }
4065
+ }
4066
+ process.stdout.write("\x1B[?1049h");
4067
+ process.stdout.write("\x1B[?25l");
4068
+ enableSyncOutput();
4069
+ const { waitUntilExit } = render2(
4070
+ React6.createElement(MonitorApp, { serverUrl, logPath }),
4071
+ { patchConsole: false }
4072
+ );
4073
+ await waitUntilExit();
4074
+ disableSyncOutput();
4075
+ process.stdout.write("\x1B[?25h");
4076
+ process.stdout.write("\x1B[?1049l");
4077
+ }
4078
+
4079
+ // src/commands/remote.ts
4080
+ import { writeFile as writeFile4 } from "fs/promises";
4081
+ import { resolve as resolve7 } from "path";
4082
+
4083
+ // src/docker/templates.ts
4084
+ function generateDockerfile(port) {
4085
+ return `FROM node:20-alpine
4086
+ WORKDIR /app
4087
+
4088
+ # Copy config and data files
4089
+ COPY pipequery.yaml .
4090
+ COPY data/ data/ 2>/dev/null || true
4091
+
4092
+ # Install PipeQuery CLI
4093
+ RUN npm install -g @andreadito/pq
4094
+
4095
+ EXPOSE ${port}
4096
+
4097
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \\
4098
+ CMD wget -qO- http://localhost:${port}/health || exit 1
4099
+
4100
+ CMD ["pq", "serve", "--host", "0.0.0.0"]
4101
+ `;
4102
+ }
4103
+ function generateDockerCompose(port) {
4104
+ return `services:
4105
+ pipequery:
4106
+ build: .
4107
+ ports:
4108
+ - "${port}:${port}"
4109
+ volumes:
4110
+ - ./pipequery.yaml:/app/pipequery.yaml:ro
4111
+ - ./data:/app/data:ro
4112
+ restart: unless-stopped
4113
+ environment:
4114
+ - NODE_ENV=production
4115
+ `;
4116
+ }
4117
+ function generateDockerignore() {
4118
+ return `node_modules
4119
+ .pipequery
4120
+ .git
4121
+ *.log
4122
+ dist
4123
+ `;
4124
+ }
4125
+
4126
+ // src/commands/remote.ts
4127
+ async function remoteDeployCommand() {
4128
+ const cwd = process.cwd();
4129
+ const { config } = await loadConfig(cwd);
4130
+ const port = config.server.port;
4131
+ await writeFile4(resolve7(cwd, "Dockerfile"), generateDockerfile(port));
4132
+ await writeFile4(resolve7(cwd, "docker-compose.yaml"), generateDockerCompose(port));
4133
+ await writeFile4(resolve7(cwd, ".dockerignore"), generateDockerignore());
4134
+ log.success("Generated Dockerfile, docker-compose.yaml, and .dockerignore");
4135
+ log.info(`Run: docker compose up -d --build`);
4136
+ log.info(`Then: pq remote connect http://localhost:${port}`);
4137
+ }
4138
+ async function loadOrCreateConfig(cwd) {
4139
+ const existing = findConfigPath(cwd);
4140
+ if (existing) {
4141
+ return loadConfig(cwd);
4142
+ }
4143
+ const configPath = resolve7(cwd, "pipequery.yaml");
4144
+ const config = {
4145
+ server: { ...DEFAULT_CONFIG.server },
4146
+ sources: {},
4147
+ endpoints: {},
4148
+ dashboards: {}
4149
+ };
4150
+ await saveConfig(config, configPath);
4151
+ return { config, path: configPath };
4152
+ }
4153
+ async function remoteConnectCommand(url) {
4154
+ try {
4155
+ const res = await fetch(`${url}/health`);
4156
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
4157
+ } catch (err) {
4158
+ log.error(`Cannot reach ${url}: ${err instanceof Error ? err.message : err}`);
4159
+ return;
4160
+ }
4161
+ const cwd = process.cwd();
4162
+ const { config, path: configPath } = await loadOrCreateConfig(cwd);
4163
+ config.remote = { url };
4164
+ await saveConfig(config, configPath);
4165
+ log.success(`Connected to remote server at ${url}`);
4166
+ }
4167
+ async function remoteStatusCommand() {
4168
+ const cwd = process.cwd();
4169
+ const existing = findConfigPath(cwd);
4170
+ if (!existing) {
4171
+ log.error("No remote server configured. Use: pq remote connect <url>");
4172
+ return;
4173
+ }
4174
+ const { config } = await loadConfig(cwd);
4175
+ const url = config.remote?.url;
4176
+ if (!url) {
4177
+ log.error("No remote server configured. Use: pq remote connect <url>");
4178
+ return;
4179
+ }
4180
+ try {
4181
+ const res = await fetch(`${url}/status`);
4182
+ const status = await res.json();
4183
+ log.success(`Remote server at ${url}`);
4184
+ log.info(`Uptime: ${Math.floor(status.uptime / 1e3)}s`);
4185
+ log.info(`Sources: ${Object.keys(status.sources).length}`);
4186
+ } catch (err) {
4187
+ log.error(`Cannot reach ${url}: ${err instanceof Error ? err.message : err}`);
4188
+ }
4189
+ }
4190
+
4191
+ // src/commands/completion.ts
4192
+ var BASH_COMPLETION = `###-begin-pq-completions-###
4193
+ _pq_completions() {
4194
+ local cur prev commands source_cmds endpoint_cmds remote_cmds
4195
+ COMPREPLY=()
4196
+ cur="\${COMP_WORDS[COMP_CWORD]}"
4197
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
4198
+
4199
+ commands="init serve query source endpoint dashboard remote help"
4200
+ source_cmds="list add remove test"
4201
+ endpoint_cmds="list add remove"
4202
+ remote_cmds="deploy connect status"
4203
+
4204
+ case "\${COMP_WORDS[1]}" in
4205
+ source)
4206
+ COMPREPLY=( $(compgen -W "$source_cmds" -- "$cur") )
4207
+ return 0
4208
+ ;;
4209
+ endpoint)
4210
+ COMPREPLY=( $(compgen -W "$endpoint_cmds" -- "$cur") )
4211
+ return 0
4212
+ ;;
4213
+ remote)
4214
+ COMPREPLY=( $(compgen -W "$remote_cmds" -- "$cur") )
4215
+ return 0
4216
+ ;;
4217
+ serve)
4218
+ COMPREPLY=( $(compgen -W "--port --host --daemon --stop" -- "$cur") )
4219
+ return 0
4220
+ ;;
4221
+ query)
4222
+ COMPREPLY=( $(compgen -W "--format" -- "$cur") )
4223
+ return 0
4224
+ ;;
4225
+ dashboard)
4226
+ COMPREPLY=( $(compgen -W "--name" -- "$cur") )
4227
+ return 0
4228
+ ;;
4229
+ esac
4230
+
4231
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
4232
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
4233
+ fi
4234
+
4235
+ return 0
4236
+ }
4237
+ complete -F _pq_completions pq
4238
+ ###-end-pq-completions-###`;
4239
+ var ZSH_COMPLETION = `###-begin-pq-completions-###
4240
+ _pq() {
4241
+ local -a commands source_cmds endpoint_cmds remote_cmds
4242
+
4243
+ commands=(
4244
+ 'init:Create a pipequery.yaml config file'
4245
+ 'serve:Start the PipeQuery server'
4246
+ 'query:Run an ad-hoc PipeQuery expression'
4247
+ 'source:Manage data sources'
4248
+ 'endpoint:Manage API endpoints'
4249
+ 'dashboard:Launch the interactive terminal dashboard'
4250
+ 'remote:Manage remote deployment'
4251
+ )
4252
+
4253
+ source_cmds=(
4254
+ 'list:List configured data sources'
4255
+ 'add:Add a data source'
4256
+ 'remove:Remove a data source'
4257
+ 'test:Test a data source'
4258
+ )
4259
+
4260
+ endpoint_cmds=(
4261
+ 'list:List API endpoints'
4262
+ 'add:Register an API endpoint'
4263
+ 'remove:Remove an API endpoint'
4264
+ )
4265
+
4266
+ remote_cmds=(
4267
+ 'deploy:Generate Dockerfile and docker-compose.yaml'
4268
+ 'connect:Connect CLI to a remote PipeQuery server'
4269
+ 'status:Check remote server health'
4270
+ )
4271
+
4272
+ _arguments -C \\
4273
+ '1: :->command' \\
4274
+ '2: :->subcommand' \\
4275
+ '*::arg:->args'
4276
+
4277
+ case $state in
4278
+ command)
4279
+ _describe 'command' commands
4280
+ ;;
4281
+ subcommand)
4282
+ case $words[2] in
4283
+ source) _describe 'subcommand' source_cmds ;;
4284
+ endpoint) _describe 'subcommand' endpoint_cmds ;;
4285
+ remote) _describe 'subcommand' remote_cmds ;;
4286
+ esac
4287
+ ;;
4288
+ esac
4289
+ }
4290
+ compdef _pq pq
4291
+ ###-end-pq-completions-###`;
4292
+ var FISH_COMPLETION = `###-begin-pq-completions-###
4293
+ # Top-level commands
4294
+ complete -c pq -f -n '__fish_use_subcommand' -a 'init' -d 'Create a pipequery.yaml config file'
4295
+ complete -c pq -f -n '__fish_use_subcommand' -a 'serve' -d 'Start the PipeQuery server'
4296
+ complete -c pq -f -n '__fish_use_subcommand' -a 'query' -d 'Run an ad-hoc PipeQuery expression'
4297
+ complete -c pq -f -n '__fish_use_subcommand' -a 'source' -d 'Manage data sources'
4298
+ complete -c pq -f -n '__fish_use_subcommand' -a 'endpoint' -d 'Manage API endpoints'
4299
+ complete -c pq -f -n '__fish_use_subcommand' -a 'dashboard' -d 'Launch the terminal dashboard'
4300
+ complete -c pq -f -n '__fish_use_subcommand' -a 'remote' -d 'Manage remote deployment'
4301
+
4302
+ # source subcommands
4303
+ complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'list' -d 'List data sources'
4304
+ complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'add' -d 'Add a data source'
4305
+ complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'remove' -d 'Remove a data source'
4306
+ complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'test' -d 'Test a data source'
4307
+
4308
+ # endpoint subcommands
4309
+ complete -c pq -f -n '__fish_seen_subcommand_from endpoint' -a 'list' -d 'List endpoints'
4310
+ complete -c pq -f -n '__fish_seen_subcommand_from endpoint' -a 'add' -d 'Add an endpoint'
4311
+ complete -c pq -f -n '__fish_seen_subcommand_from endpoint' -a 'remove' -d 'Remove an endpoint'
4312
+
4313
+ # remote subcommands
4314
+ complete -c pq -f -n '__fish_seen_subcommand_from remote' -a 'deploy' -d 'Generate Docker files'
4315
+ complete -c pq -f -n '__fish_seen_subcommand_from remote' -a 'connect' -d 'Connect to remote server'
4316
+ complete -c pq -f -n '__fish_seen_subcommand_from remote' -a 'status' -d 'Check remote health'
4317
+
4318
+ # serve flags
4319
+ complete -c pq -l port -s p -n '__fish_seen_subcommand_from serve' -d 'Server port'
4320
+ complete -c pq -l host -s H -n '__fish_seen_subcommand_from serve' -d 'Server host'
4321
+ complete -c pq -l daemon -s d -n '__fish_seen_subcommand_from serve' -d 'Run as daemon'
4322
+ complete -c pq -l stop -n '__fish_seen_subcommand_from serve' -d 'Stop running daemon'
4323
+ ###-end-pq-completions-###`;
4324
+ async function completionCommand(opts) {
4325
+ const shell = opts.shell ?? detectShell();
4326
+ switch (shell) {
4327
+ case "bash":
4328
+ console.log(BASH_COMPLETION);
4329
+ log.dim("\n# Add to ~/.bashrc:");
4330
+ log.dim('# eval "$(pq completion --shell bash)"');
4331
+ break;
4332
+ case "zsh":
4333
+ console.log(ZSH_COMPLETION);
4334
+ log.dim("\n# Add to ~/.zshrc:");
4335
+ log.dim('# eval "$(pq completion --shell zsh)"');
4336
+ break;
4337
+ case "fish":
4338
+ console.log(FISH_COMPLETION);
4339
+ log.dim("\n# Save to ~/.config/fish/completions/pq.fish:");
4340
+ log.dim("# pq completion --shell fish > ~/.config/fish/completions/pq.fish");
4341
+ break;
4342
+ default:
4343
+ log.error(`Unknown shell: ${shell}. Use --shell bash|zsh|fish`);
4344
+ }
4345
+ }
4346
+ function detectShell() {
4347
+ const shell = process.env.SHELL ?? "";
4348
+ if (shell.includes("zsh")) return "zsh";
4349
+ if (shell.includes("fish")) return "fish";
4350
+ return "bash";
4351
+ }
4352
+
4353
+ // src/commands/stop.ts
4354
+ import { readFile as readFile5, unlink as unlink2 } from "fs/promises";
4355
+ import { resolve as resolve8 } from "path";
4356
+ import { existsSync as existsSync6 } from "fs";
4357
+ async function stopCommand(opts) {
4358
+ const cwd = process.cwd();
4359
+ const serverJsonPath = resolve8(cwd, ".pipequery", "server.json");
4360
+ if (!existsSync6(serverJsonPath)) {
4361
+ log.error("No running server found.");
4362
+ log.dim("Start one with: pq serve or pq serve -d");
4363
+ return;
4364
+ }
4365
+ let info2;
4366
+ try {
4367
+ info2 = JSON.parse(await readFile5(serverJsonPath, "utf-8"));
4368
+ } catch {
4369
+ log.error("Could not read server info. Cleaning up stale file.");
4370
+ try {
4371
+ await unlink2(serverJsonPath);
4372
+ } catch {
4373
+ }
4374
+ return;
4375
+ }
4376
+ let alive = false;
4377
+ try {
4378
+ process.kill(info2.pid, 0);
4379
+ alive = true;
4380
+ } catch {
4381
+ }
4382
+ if (!alive) {
4383
+ log.warn(`Server (PID: ${info2.pid}) is not running. Cleaning up stale state.`);
4384
+ try {
4385
+ await unlink2(serverJsonPath);
4386
+ } catch {
4387
+ }
4388
+ return;
4389
+ }
4390
+ const signal = opts.force ? "SIGKILL" : "SIGTERM";
4391
+ try {
4392
+ process.kill(info2.pid, signal);
4393
+ log.success(`Server stopped (PID: ${info2.pid})${info2.daemon ? " [daemon]" : ""}`);
4394
+ } catch (err) {
4395
+ log.error(`Failed to stop server: ${err instanceof Error ? err.message : err}`);
4396
+ }
4397
+ try {
4398
+ await unlink2(serverJsonPath);
4399
+ } catch {
4400
+ }
4401
+ }
4402
+
4403
+ // src/commands/repl.ts
4404
+ import { createInterface } from "readline";
4405
+ import { spawnSync } from "child_process";
4406
+ import chalk7 from "chalk";
4407
+ var COMMANDS = {
4408
+ help: "Show available commands",
4409
+ serve: "Start the server (serve -d for daemon, serve --stop to stop)",
4410
+ query: "Run a PipeQuery expression (e.g. query products | first(5))",
4411
+ sources: "List data sources",
4412
+ endpoints: "List API endpoints",
4413
+ status: "Show server status",
4414
+ dashboard: "Launch the interactive TUI dashboard",
4415
+ monitor: "Launch the live server monitor",
4416
+ clear: "Clear the screen",
4417
+ exit: "Exit the REPL"
4418
+ };
4419
+ async function startRepl() {
4420
+ printCompactBanner();
4421
+ let serverUrl = null;
4422
+ try {
4423
+ serverUrl = await getServerUrl();
4424
+ log.success(`Connected to server at ${chalk7.white.bold(serverUrl)}`);
4425
+ } catch {
4426
+ log.warn("No running server found. Start one with: pq serve");
4427
+ }
4428
+ console.log(chalk7.dim(' Type "help" for available commands, "exit" to quit.\n'));
4429
+ const rl = createInterface({
4430
+ input: process.stdin,
4431
+ output: process.stdout,
4432
+ prompt: chalk7.hex("#06b6d4").bold(" pq > "),
4433
+ terminal: true
4434
+ });
4435
+ rl.prompt();
4436
+ rl.on("line", async (line) => {
4437
+ const input = line.trim();
4438
+ if (!input) {
4439
+ rl.prompt();
4440
+ return;
4441
+ }
4442
+ const [cmd, ...rest] = input.split(/\s+/);
4443
+ const args = rest.join(" ");
4444
+ try {
4445
+ switch (cmd) {
4446
+ case "help":
4447
+ console.log();
4448
+ for (const [name, desc] of Object.entries(COMMANDS)) {
4449
+ console.log(` ${chalk7.hex("#06b6d4")(name.padEnd(12))} ${chalk7.dim(desc)}`);
4450
+ }
4451
+ console.log();
4452
+ break;
4453
+ case "query":
4454
+ case "q":
4455
+ if (!args) {
4456
+ log.warn("Usage: query <expression>");
4457
+ break;
4458
+ }
4459
+ if (!serverUrl) {
4460
+ log.error("No server running. Start one with: pq serve");
4461
+ break;
4462
+ }
4463
+ await runQuery(serverUrl, args);
4464
+ break;
4465
+ case "sources":
4466
+ if (!serverUrl) {
4467
+ log.error("No server running.");
4468
+ break;
4469
+ }
4470
+ await fetchAndPrint(serverUrl, "/api/_control/sources", "sources");
4471
+ break;
4472
+ case "endpoints":
4473
+ if (!serverUrl) {
4474
+ log.error("No server running.");
4475
+ break;
4476
+ }
4477
+ await fetchAndPrint(serverUrl, "/api/_control/endpoints", "endpoints");
4478
+ break;
4479
+ case "status":
4480
+ if (!serverUrl) {
4481
+ log.error("No server running.");
4482
+ break;
4483
+ }
4484
+ await fetchAndPrint(serverUrl, "/status", null);
4485
+ break;
4486
+ case "serve": {
4487
+ const serveArgs = ["serve"];
4488
+ if (args) serveArgs.push(...args.split(/\s+/));
4489
+ rl.pause();
4490
+ spawnSync(process.execPath, [process.argv[1], ...serveArgs], {
4491
+ cwd: process.cwd(),
4492
+ stdio: "inherit"
4493
+ });
4494
+ try {
4495
+ serverUrl = await getServerUrl();
4496
+ log.success(`Connected to server at ${chalk7.white.bold(serverUrl)}`);
4497
+ } catch {
4498
+ }
4499
+ rl.resume();
4500
+ break;
4501
+ }
4502
+ case "dashboard":
4503
+ rl.pause();
4504
+ spawnSync(process.execPath, [process.argv[1], "dashboard", "-n", args || "main"], {
4505
+ cwd: process.cwd(),
4506
+ stdio: "inherit"
4507
+ });
4508
+ rl.resume();
4509
+ break;
4510
+ case "monitor":
4511
+ rl.pause();
4512
+ spawnSync(process.execPath, [process.argv[1], "monitor"], {
4513
+ cwd: process.cwd(),
4514
+ stdio: "inherit"
4515
+ });
4516
+ rl.resume();
4517
+ break;
4518
+ case "clear":
4519
+ process.stdout.write("\x1B[2J\x1B[H");
4520
+ printCompactBanner();
4521
+ break;
4522
+ case "exit":
4523
+ case "quit":
4524
+ console.log(chalk7.dim(" Goodbye.\n"));
4525
+ rl.close();
4526
+ process.exit(0);
4527
+ break;
4528
+ default:
4529
+ log.warn(`Unknown command: ${cmd}. Type "help" for available commands.`);
4530
+ }
4531
+ } catch (err) {
4532
+ log.error(err instanceof Error ? err.message : String(err));
4533
+ }
4534
+ rl.prompt();
4535
+ });
4536
+ rl.on("close", () => {
4537
+ process.exit(0);
4538
+ });
4539
+ }
4540
+ async function runQuery(serverUrl, expression) {
4541
+ const res = await fetch(`${serverUrl}/api/_control/query`, {
4542
+ method: "POST",
4543
+ headers: { "Content-Type": "application/json" },
4544
+ body: JSON.stringify({ query: expression })
4545
+ });
4546
+ const json = await res.json();
4547
+ if (!json.ok) {
4548
+ log.error(json.error ?? "Query failed");
4549
+ return;
4550
+ }
4551
+ const result = json.result;
4552
+ if (Array.isArray(result) && result.length > 0 && typeof result[0] === "object") {
4553
+ printTable(result);
4554
+ } else {
4555
+ console.log(JSON.stringify(result, null, 2));
4556
+ }
4557
+ }
4558
+ function printTable(rows) {
4559
+ if (rows.length === 0) {
4560
+ log.dim("(empty result)");
4561
+ return;
4562
+ }
4563
+ const columns = Object.keys(rows[0]);
4564
+ const widths = columns.map((col) => {
4565
+ const maxData = rows.reduce((max, row) => Math.max(max, formatCell3(row[col]).length), 0);
4566
+ return Math.max(col.length, maxData);
4567
+ });
4568
+ const header = columns.map((col, i) => chalk7.hex("#06b6d4").bold(col.padEnd(widths[i]))).join(" ");
4569
+ const separator = widths.map((w) => chalk7.dim("\u2500".repeat(w))).join(chalk7.dim("\u2500\u2500"));
4570
+ console.log();
4571
+ console.log(` ${header}`);
4572
+ console.log(` ${separator}`);
4573
+ for (const row of rows) {
4574
+ const line = columns.map((col, i) => {
4575
+ const val = formatCell3(row[col]);
4576
+ return typeof row[col] === "number" ? chalk7.hex("#f59e0b")(val.padStart(widths[i])) : val.padEnd(widths[i]);
4577
+ }).join(" ");
4578
+ console.log(` ${line}`);
4579
+ }
4580
+ console.log();
4581
+ console.log(chalk7.dim(` ${rows.length} row(s)`));
4582
+ console.log();
4583
+ }
4584
+ function formatCell3(value) {
4585
+ if (value == null) return "";
4586
+ if (typeof value === "number") return Number.isInteger(value) ? String(value) : value.toFixed(2);
4587
+ return String(value);
4588
+ }
4589
+ async function fetchAndPrint(serverUrl, path, key) {
4590
+ const res = await fetch(`${serverUrl}${path}`);
4591
+ const json = await res.json();
4592
+ const data = key ? json[key] : json;
4593
+ console.log();
4594
+ console.log(JSON.stringify(data, null, 2));
4595
+ console.log();
4596
+ }
4597
+
4598
+ // src/index.ts
4599
+ var program = new Command();
4600
+ program.name("pq").description("PipeQuery CLI \u2014 data sources, aggregations, APIs, and terminal dashboards").version("0.1.0").addHelpText("beforeAll", () => {
4601
+ printBanner();
4602
+ return "";
4603
+ }).configureHelp({
4604
+ subcommandTerm: (cmd) => chalk8.hex("#06b6d4")(cmd.name()) + (cmd.usage() ? " " + chalk8.dim(cmd.usage()) : "")
4605
+ });
4606
+ program.command("init").description("Create a pipequery.yaml config file").action(initCommand);
4607
+ program.command("serve").description("Start the PipeQuery server").option("-p, --port <port>", "Server port", parseInt).option("-H, --host <host>", "Server host").option("-d, --daemon", "Run as a background daemon").option("--stop", "Stop a running daemon").action(serveCommand);
4608
+ program.command("query <expression>").description("Run an ad-hoc PipeQuery expression").option("-f, --format <format>", "Output format: table, json", "table").action(queryCommand);
4609
+ var source = program.command("source").description("Manage data sources");
4610
+ source.command("list").description("List configured data sources").action(sourceListCommand);
4611
+ source.command("add <name>").description("Add a data source").requiredOption("-t, --type <type>", "Source type: rest, websocket, file").option("-u, --url <url>", "URL for REST or WebSocket sources").option("-i, --interval <interval>", "Poll interval for REST (e.g., 30s, 5m)", "30s").option("-d, --data-path <path>", "Dot-path to extract data from response").option("-p, --path <path>", "File path for file sources").option("-w, --watch", "Watch file for changes").action(sourceAddCommand);
4612
+ source.command("remove <name>").description("Remove a data source").action(sourceRemoveCommand);
4613
+ source.command("test <name>").description("Test a data source and show sample data").action(sourceTestCommand);
4614
+ var endpoint = program.command("endpoint").description("Manage API endpoints");
4615
+ endpoint.command("list").description("List API endpoints").action(endpointListCommand);
4616
+ endpoint.command("add <path>").description("Register an API endpoint").requiredOption("-q, --query <query>", "PipeQuery expression").option("-c, --cache <duration>", "Cache duration (e.g., 30s)").action(endpointAddCommand);
4617
+ endpoint.command("remove <path>").description("Remove an API endpoint").action(endpointRemoveCommand);
4618
+ program.command("dashboard").description("Launch the interactive terminal dashboard").option("-n, --name <name>", "Dashboard name from config", "main").action(dashboardCommand);
4619
+ program.command("monitor").description("Live server monitor \u2014 sources, endpoints, logs").action(monitorCommand);
4620
+ var remote = program.command("remote").description("Manage remote deployment");
4621
+ remote.command("deploy").description("Generate Dockerfile and docker-compose.yaml").action(remoteDeployCommand);
4622
+ remote.command("connect <url>").description("Connect CLI to a remote PipeQuery server").action(remoteConnectCommand);
4623
+ remote.command("status").description("Check remote server health").action(remoteStatusCommand);
4624
+ program.command("stop").description("Stop the running PipeQuery server (daemon or foreground)").option("-f, --force", "Force kill (SIGKILL instead of SIGTERM)").action(stopCommand);
4625
+ program.command("completion").description("Generate shell completion script").option("-s, --shell <shell>", "Shell type: bash, zsh, fish").action(completionCommand);
4626
+ if (process.argv.length <= 2) {
4627
+ startRepl();
4628
+ } else {
4629
+ program.parse();
4630
+ }