@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.
- package/README.md +384 -0
- package/dist/index.js +4630 -0
- package/package.json +43 -0
- package/src/commands/completion.ts +167 -0
- package/src/commands/dashboard.ts +44 -0
- package/src/commands/endpoint.ts +99 -0
- package/src/commands/init.ts +20 -0
- package/src/commands/monitor.ts +52 -0
- package/src/commands/query.ts +81 -0
- package/src/commands/remote.ts +89 -0
- package/src/commands/repl.ts +222 -0
- package/src/commands/serve.ts +122 -0
- package/src/commands/source.ts +150 -0
- package/src/commands/stop.ts +50 -0
- package/src/config/defaults.ts +48 -0
- package/src/config/loader.ts +37 -0
- package/src/config/schema.ts +73 -0
- package/src/docker/templates.ts +43 -0
- package/src/engine.ts +5 -0
- package/src/index.ts +164 -0
- package/src/server/activity-log.ts +28 -0
- package/src/server/app.ts +101 -0
- package/src/server/cache.ts +30 -0
- package/src/server/routes/control.ts +100 -0
- package/src/server/routes/dynamic.ts +51 -0
- package/src/server/routes/health.ts +16 -0
- package/src/server/routes/sse.ts +96 -0
- package/src/server/sources/file.ts +79 -0
- package/src/server/sources/manager.ts +120 -0
- package/src/server/sources/rest.ts +85 -0
- package/src/server/sources/static.ts +31 -0
- package/src/server/sources/types.ts +15 -0
- package/src/server/sources/websocket.ts +86 -0
- package/src/tui/App.tsx +141 -0
- package/src/tui/MonitorApp.tsx +271 -0
- package/src/tui/components/BarChart.tsx +48 -0
- package/src/tui/components/CandleChart.tsx +133 -0
- package/src/tui/components/Heatmap.tsx +92 -0
- package/src/tui/components/Layout.tsx +42 -0
- package/src/tui/components/OrderBook.tsx +94 -0
- package/src/tui/components/Panel.tsx +35 -0
- package/src/tui/components/Sparkline.tsx +55 -0
- package/src/tui/components/StatBox.tsx +37 -0
- package/src/tui/components/StatusBar.tsx +51 -0
- package/src/tui/components/Table.tsx +133 -0
- package/src/tui/hooks/useBatchedData.ts +77 -0
- package/src/tui/hooks/useKeyboard.ts +23 -0
- package/src/tui/hooks/useLiveData.ts +161 -0
- package/src/tui/hooks/useMouse.ts +86 -0
- package/src/tui/hooks/useSSE.ts +95 -0
- package/src/tui/hooks/useServerData.ts +37 -0
- package/src/utils/banner.ts +70 -0
- package/src/utils/logger.ts +10 -0
- package/src/utils/parseDuration.ts +12 -0
- package/src/utils/server-discovery.ts +43 -0
- package/src/utils/sync-output.ts +62 -0
- package/src/utils/yaml.ts +12 -0
- package/tsconfig.json +17 -0
- 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