@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
@@ -0,0 +1,95 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import http from 'node:http';
3
+
4
+ interface SSEState {
5
+ results: Map<number, unknown>;
6
+ errors: Map<number, string>;
7
+ connected: boolean;
8
+ }
9
+
10
+ /**
11
+ * Hook that connects to the SSE endpoint for real-time dashboard updates.
12
+ * Falls back to polling if SSE connection fails.
13
+ */
14
+ export function useSSE(serverUrl: string, queries: string[]) {
15
+ const [state, setState] = useState<SSEState>({
16
+ results: new Map(),
17
+ errors: new Map(),
18
+ connected: false,
19
+ });
20
+ const abortRef = useRef<AbortController | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (queries.length === 0) return;
24
+
25
+ const abort = new AbortController();
26
+ abortRef.current = abort;
27
+
28
+ const url = new URL('/api/_control/sse', serverUrl);
29
+ url.searchParams.set('queries', JSON.stringify(queries));
30
+
31
+ // Use Node.js http for SSE (EventSource not available in Node)
32
+ const connect = () => {
33
+ const req = http.get(url.toString(), { signal: abort.signal }, (res) => {
34
+ if (res.statusCode !== 200) {
35
+ setState((s) => ({ ...s, connected: false }));
36
+ return;
37
+ }
38
+
39
+ setState((s) => ({ ...s, connected: true }));
40
+
41
+ let buffer = '';
42
+ res.on('data', (chunk: Buffer) => {
43
+ buffer += chunk.toString();
44
+ const lines = buffer.split('\n\n');
45
+ buffer = lines.pop() ?? '';
46
+
47
+ for (const block of lines) {
48
+ const dataLine = block.split('\n').find((l) => l.startsWith('data: '));
49
+ if (!dataLine) continue;
50
+
51
+ try {
52
+ const event = JSON.parse(dataLine.slice(6)) as { panelIndex: number; result?: unknown; error?: string };
53
+ setState((s) => {
54
+ const results = new Map(s.results);
55
+ const errors = new Map(s.errors);
56
+ if (event.error) {
57
+ errors.set(event.panelIndex, event.error);
58
+ } else {
59
+ results.set(event.panelIndex, event.result);
60
+ errors.delete(event.panelIndex);
61
+ }
62
+ return { results, errors, connected: true };
63
+ });
64
+ } catch {
65
+ // Ignore parse errors (heartbeats etc.)
66
+ }
67
+ }
68
+ });
69
+
70
+ res.on('end', () => {
71
+ setState((s) => ({ ...s, connected: false }));
72
+ // Reconnect after 2s
73
+ if (!abort.signal.aborted) {
74
+ setTimeout(connect, 2000);
75
+ }
76
+ });
77
+ });
78
+
79
+ req.on('error', () => {
80
+ setState((s) => ({ ...s, connected: false }));
81
+ if (!abort.signal.aborted) {
82
+ setTimeout(connect, 2000);
83
+ }
84
+ });
85
+ };
86
+
87
+ connect();
88
+
89
+ return () => {
90
+ abort.abort();
91
+ };
92
+ }, [serverUrl, queries.join(',')]);
93
+
94
+ return state;
95
+ }
@@ -0,0 +1,37 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ interface PanelData {
4
+ data: unknown;
5
+ loading: boolean;
6
+ error?: string;
7
+ }
8
+
9
+ export function useServerData(serverUrl: string, query: string, refreshMs: number) {
10
+ const [state, setState] = useState<PanelData>({ data: null, loading: true });
11
+
12
+ const fetchData = useCallback(async () => {
13
+ try {
14
+ const res = await fetch(`${serverUrl}/api/_control/query`, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({ query }),
18
+ });
19
+ const json = await res.json() as { ok: boolean; result?: unknown; error?: string };
20
+ if (json.ok) {
21
+ setState({ data: json.result, loading: false });
22
+ } else {
23
+ setState({ data: null, loading: false, error: json.error });
24
+ }
25
+ } catch (err) {
26
+ setState({ data: null, loading: false, error: err instanceof Error ? err.message : String(err) });
27
+ }
28
+ }, [serverUrl, query]);
29
+
30
+ useEffect(() => {
31
+ fetchData();
32
+ const timer = setInterval(fetchData, refreshMs);
33
+ return () => clearInterval(timer);
34
+ }, [fetchData, refreshMs]);
35
+
36
+ return { ...state, refresh: fetchData };
37
+ }
@@ -0,0 +1,70 @@
1
+ import chalk from 'chalk';
2
+
3
+ const LOGO = [
4
+ ' ██████╗ ██╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗ ███████╗ ██████╗ ██╗ ██╗',
5
+ ' ██╔══██╗ ██║ ██╔══██╗ ██╔════╝ ██╔═══██╗ ██║ ██║ ██╔════╝ ██╔══██╗ ╚██╗ ██╔╝',
6
+ ' ██████╔╝ ██║ ██████╔╝ █████╗ ██║ ██║ ██║ ██║ █████╗ ██████╔╝ ╚████╔╝ ',
7
+ ' ██╔═══╝ ██║ ██╔═══╝ ██╔══╝ ██║▄▄ ██║ ██║ ██║ ██╔══╝ ██╔══██╗ ╚██╔╝ ',
8
+ ' ██║ ██║ ██║ ███████╗ ╚██████╔╝ ╚██████╔╝ ███████╗ ██║ ██║ ██║ ',
9
+ ' ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ',
10
+ ];
11
+
12
+ // Gradient from cyan → blue → magenta
13
+ const GRADIENT_COLORS = [
14
+ '#06b6d4', // cyan
15
+ '#0891b2',
16
+ '#0e7490',
17
+ '#2563eb', // blue
18
+ '#4f46e5',
19
+ '#7c3aed', // violet
20
+ ];
21
+
22
+ function applyGradient(line: string, colorIndex: number): string {
23
+ const color = GRADIENT_COLORS[colorIndex % GRADIENT_COLORS.length];
24
+ return chalk.hex(color)(line);
25
+ }
26
+
27
+ export function printBanner() {
28
+ console.log();
29
+ LOGO.forEach((line, i) => {
30
+ console.log(applyGradient(line, i));
31
+ });
32
+ console.log();
33
+ console.log(chalk.dim(` v0.1.0 — data sources, aggregations, APIs, and terminal dashboards`));
34
+ console.log();
35
+ }
36
+
37
+ export function printCompactBanner() {
38
+ console.log();
39
+ console.log(
40
+ chalk.hex('#06b6d4').bold(' ╔═╗ ╦ ╔═╗ ╔═╗ ╔═╗ ╦ ╦ ╔═╗ ╦═╗ ╦ ╦'),
41
+ );
42
+ console.log(
43
+ chalk.hex('#4f46e5').bold(' ╠═╝ ║ ╠═╝ ║╣ ║═╬╗║ ║ ║╣ ╠╦╝ ╚╦╝'),
44
+ );
45
+ console.log(
46
+ chalk.hex('#7c3aed').bold(' ╩ ╩ ╩ ╚═╝ ╚═╝╚╚═╝ ╚═╝ ╩╚═ ╩ '),
47
+ );
48
+ console.log();
49
+ }
50
+
51
+ export function printServerStartup(opts: {
52
+ url: string;
53
+ sourceCount: number;
54
+ endpointCount: number;
55
+ }) {
56
+ const { url, sourceCount, endpointCount } = opts;
57
+
58
+ console.log(chalk.hex('#06b6d4').bold(' ● Server running'));
59
+ console.log();
60
+ console.log(` ${chalk.dim('URL')} ${chalk.white.bold(url)}`);
61
+ console.log(` ${chalk.dim('Sources')} ${chalk.white(String(sourceCount))} loaded`);
62
+ console.log(` ${chalk.dim('Endpoints')} ${chalk.white(String(endpointCount))} registered`);
63
+ console.log();
64
+ console.log(chalk.dim(' Press Ctrl+C to stop'));
65
+ console.log();
66
+ }
67
+
68
+ export function printSection(title: string) {
69
+ console.log(chalk.hex('#4f46e5').bold(` ${title}`));
70
+ }
@@ -0,0 +1,10 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const log = {
4
+ info: (msg: string) => console.log(chalk.hex('#06b6d4')(' ●'), msg),
5
+ success: (msg: string) => console.log(chalk.hex('#10b981')(' ✓'), msg),
6
+ warn: (msg: string) => console.log(chalk.hex('#f59e0b')(' ⚠'), msg),
7
+ error: (msg: string) => console.error(chalk.hex('#ef4444')(' ✗'), msg),
8
+ dim: (msg: string) => console.log(chalk.dim(` ${msg}`)),
9
+ step: (msg: string) => console.log(chalk.hex('#4f46e5')(' →'), msg),
10
+ };
@@ -0,0 +1,12 @@
1
+ const UNITS: Record<string, number> = {
2
+ ms: 1,
3
+ s: 1000,
4
+ m: 60_000,
5
+ h: 3_600_000,
6
+ };
7
+
8
+ export function parseDuration(input: string): number {
9
+ const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/);
10
+ if (!match) throw new Error(`Invalid duration: "${input}". Use format like "30s", "5m", "1h".`);
11
+ return parseFloat(match[1]) * UNITS[match[2]];
12
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { findConfigPath } from '../config/loader.js';
5
+ import { readYaml } from './yaml.js';
6
+
7
+ interface ServerInfo {
8
+ url: string;
9
+ pid: number;
10
+ }
11
+
12
+ export async function getServerUrl(): Promise<string> {
13
+ const cwd = process.cwd();
14
+
15
+ // 1. Check .pipequery/server.json for running local server
16
+ const serverJsonPath = resolve(cwd, '.pipequery', 'server.json');
17
+ if (existsSync(serverJsonPath)) {
18
+ try {
19
+ const info = JSON.parse(await readFile(serverJsonPath, 'utf-8')) as ServerInfo;
20
+ // Verify the server is actually running
21
+ const res = await fetch(`${info.url}/health`).catch(() => null);
22
+ if (res?.ok) return info.url;
23
+ } catch {
24
+ // Fall through
25
+ }
26
+ }
27
+
28
+ // 2. Check config for remote URL
29
+ const configPath = findConfigPath(cwd);
30
+ if (configPath) {
31
+ try {
32
+ const config = await readYaml<{ remote?: { url: string } }>(configPath);
33
+ if (config.remote?.url) {
34
+ const res = await fetch(`${config.remote.url}/health`).catch(() => null);
35
+ if (res?.ok) return config.remote.url;
36
+ }
37
+ } catch {
38
+ // Fall through
39
+ }
40
+ }
41
+
42
+ throw new Error('No running PipeQuery server found. Start one with: pq serve');
43
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Patches process.stdout.write to use Synchronized Output protocol.
3
+ * This prevents flickering in terminals that support it (iTerm2, kitty, etc).
4
+ *
5
+ * The protocol wraps each write in Begin/End markers so the terminal
6
+ * buffers the output and paints it all at once instead of progressively.
7
+ *
8
+ * See: https://gist.github.com/christianparpart/d8a62cc1ab659194571cd72825afb6e0
9
+ */
10
+
11
+ const BEGIN_SYNC = '\x1B[?2026h';
12
+ const END_SYNC = '\x1B[?2026l';
13
+
14
+ let patched = false;
15
+ let pendingTimer: ReturnType<typeof setTimeout> | null = null;
16
+ let inSync = false;
17
+
18
+ export function enableSyncOutput() {
19
+ if (patched) return;
20
+ patched = true;
21
+
22
+ const originalWrite = process.stdout.write.bind(process.stdout);
23
+
24
+ process.stdout.write = function (
25
+ chunk: string | Uint8Array,
26
+ encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
27
+ callback?: (err?: Error | null) => void,
28
+ ): boolean {
29
+ // Start a synchronized update block
30
+ if (!inSync) {
31
+ inSync = true;
32
+ originalWrite(BEGIN_SYNC);
33
+ }
34
+
35
+ // Clear any pending end-sync timer
36
+ if (pendingTimer) {
37
+ clearTimeout(pendingTimer);
38
+ }
39
+
40
+ // Schedule ending the sync block after a microtask
41
+ // This batches all writes in the same tick
42
+ pendingTimer = setTimeout(() => {
43
+ originalWrite(END_SYNC);
44
+ inSync = false;
45
+ pendingTimer = null;
46
+ }, 0);
47
+
48
+ return originalWrite(chunk, encodingOrCallback as BufferEncoding, callback);
49
+ } as typeof process.stdout.write;
50
+ }
51
+
52
+ export function disableSyncOutput() {
53
+ // Nothing needed — process exits or we restore stdout
54
+ if (inSync) {
55
+ process.stdout.write('\x1B[?2026l');
56
+ inSync = false;
57
+ }
58
+ if (pendingTimer) {
59
+ clearTimeout(pendingTimer);
60
+ pendingTimer = null;
61
+ }
62
+ }
@@ -0,0 +1,12 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import YAML from 'yaml';
3
+
4
+ export async function readYaml<T>(path: string): Promise<T> {
5
+ const content = await readFile(path, 'utf-8');
6
+ return YAML.parse(content) as T;
7
+ }
8
+
9
+ export async function writeYaml(path: string, data: unknown): Promise<void> {
10
+ const content = YAML.stringify(data, { lineWidth: 120 });
11
+ await writeFile(path, content, 'utf-8');
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "jsx": "react-jsx",
11
+ "declaration": true,
12
+ "esModuleInterop": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true
15
+ },
16
+ "include": ["src"]
17
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ target: 'node18',
7
+ platform: 'node',
8
+ clean: true,
9
+ splitting: false,
10
+ banner: {
11
+ js: '#!/usr/bin/env node',
12
+ },
13
+ });