@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,222 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { spawnSync } from 'node:child_process';
3
+ import chalk from 'chalk';
4
+ import { printCompactBanner } from '../utils/banner.js';
5
+ import { log } from '../utils/logger.js';
6
+ import { getServerUrl } from '../utils/server-discovery.js';
7
+
8
+ const COMMANDS: Record<string, string> = {
9
+ help: 'Show available commands',
10
+ serve: 'Start the server (serve -d for daemon, serve --stop to stop)',
11
+ query: 'Run a PipeQuery expression (e.g. query products | first(5))',
12
+ sources: 'List data sources',
13
+ endpoints: 'List API endpoints',
14
+ status: 'Show server status',
15
+ dashboard: 'Launch the interactive TUI dashboard',
16
+ monitor: 'Launch the live server monitor',
17
+ clear: 'Clear the screen',
18
+ exit: 'Exit the REPL',
19
+ };
20
+
21
+ export async function startRepl() {
22
+ printCompactBanner();
23
+
24
+ let serverUrl: string | null = null;
25
+ try {
26
+ serverUrl = await getServerUrl();
27
+ log.success(`Connected to server at ${chalk.white.bold(serverUrl)}`);
28
+ } catch {
29
+ log.warn('No running server found. Start one with: pq serve');
30
+ }
31
+ console.log(chalk.dim(' Type "help" for available commands, "exit" to quit.\n'));
32
+
33
+ const rl = createInterface({
34
+ input: process.stdin,
35
+ output: process.stdout,
36
+ prompt: chalk.hex('#06b6d4').bold(' pq > '),
37
+ terminal: true,
38
+ });
39
+
40
+ rl.prompt();
41
+
42
+ rl.on('line', async (line) => {
43
+ const input = line.trim();
44
+ if (!input) {
45
+ rl.prompt();
46
+ return;
47
+ }
48
+
49
+ const [cmd, ...rest] = input.split(/\s+/);
50
+ const args = rest.join(' ');
51
+
52
+ try {
53
+ switch (cmd) {
54
+ case 'help':
55
+ console.log();
56
+ for (const [name, desc] of Object.entries(COMMANDS)) {
57
+ console.log(` ${chalk.hex('#06b6d4')(name.padEnd(12))} ${chalk.dim(desc)}`);
58
+ }
59
+ console.log();
60
+ break;
61
+
62
+ case 'query':
63
+ case 'q':
64
+ if (!args) {
65
+ log.warn('Usage: query <expression>');
66
+ break;
67
+ }
68
+ if (!serverUrl) {
69
+ log.error('No server running. Start one with: pq serve');
70
+ break;
71
+ }
72
+ await runQuery(serverUrl, args);
73
+ break;
74
+
75
+ case 'sources':
76
+ if (!serverUrl) { log.error('No server running.'); break; }
77
+ await fetchAndPrint(serverUrl, '/api/_control/sources', 'sources');
78
+ break;
79
+
80
+ case 'endpoints':
81
+ if (!serverUrl) { log.error('No server running.'); break; }
82
+ await fetchAndPrint(serverUrl, '/api/_control/endpoints', 'endpoints');
83
+ break;
84
+
85
+ case 'status':
86
+ if (!serverUrl) { log.error('No server running.'); break; }
87
+ await fetchAndPrint(serverUrl, '/status', null);
88
+ break;
89
+
90
+ case 'serve': {
91
+ const serveArgs = ['serve'];
92
+ if (args) serveArgs.push(...args.split(/\s+/));
93
+ rl.pause();
94
+ spawnSync(process.execPath, [process.argv[1], ...serveArgs], {
95
+ cwd: process.cwd(),
96
+ stdio: 'inherit',
97
+ });
98
+ // Re-check server connection after serve
99
+ try {
100
+ serverUrl = await getServerUrl();
101
+ log.success(`Connected to server at ${chalk.white.bold(serverUrl)}`);
102
+ } catch { /* server may not be running */ }
103
+ rl.resume();
104
+ break;
105
+ }
106
+
107
+ case 'dashboard':
108
+ rl.pause();
109
+ spawnSync(process.execPath, [process.argv[1], 'dashboard', '-n', args || 'main'], {
110
+ cwd: process.cwd(),
111
+ stdio: 'inherit',
112
+ });
113
+ rl.resume();
114
+ break;
115
+
116
+ case 'monitor':
117
+ rl.pause();
118
+ spawnSync(process.execPath, [process.argv[1], 'monitor'], {
119
+ cwd: process.cwd(),
120
+ stdio: 'inherit',
121
+ });
122
+ rl.resume();
123
+ break;
124
+
125
+ case 'clear':
126
+ process.stdout.write('\x1B[2J\x1B[H');
127
+ printCompactBanner();
128
+ break;
129
+
130
+ case 'exit':
131
+ case 'quit':
132
+ console.log(chalk.dim(' Goodbye.\n'));
133
+ rl.close();
134
+ process.exit(0);
135
+ break;
136
+
137
+ default:
138
+ log.warn(`Unknown command: ${cmd}. Type "help" for available commands.`);
139
+ }
140
+ } catch (err) {
141
+ log.error(err instanceof Error ? err.message : String(err));
142
+ }
143
+
144
+ rl.prompt();
145
+ });
146
+
147
+ rl.on('close', () => {
148
+ process.exit(0);
149
+ });
150
+ }
151
+
152
+ async function runQuery(serverUrl: string, expression: string) {
153
+ const res = await fetch(`${serverUrl}/api/_control/query`, {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({ query: expression }),
157
+ });
158
+
159
+ const json = (await res.json()) as { ok: boolean; result?: unknown; error?: string };
160
+ if (!json.ok) {
161
+ log.error(json.error ?? 'Query failed');
162
+ return;
163
+ }
164
+
165
+ const result = json.result;
166
+ if (Array.isArray(result) && result.length > 0 && typeof result[0] === 'object') {
167
+ printTable(result as Record<string, unknown>[]);
168
+ } else {
169
+ console.log(JSON.stringify(result, null, 2));
170
+ }
171
+ }
172
+
173
+ function printTable(rows: Record<string, unknown>[]) {
174
+ if (rows.length === 0) {
175
+ log.dim('(empty result)');
176
+ return;
177
+ }
178
+
179
+ const columns = Object.keys(rows[0]);
180
+ const widths = columns.map((col) => {
181
+ const maxData = rows.reduce((max, row) => Math.max(max, formatCell(row[col]).length), 0);
182
+ return Math.max(col.length, maxData);
183
+ });
184
+
185
+ const header = columns.map((col, i) => chalk.hex('#06b6d4').bold(col.padEnd(widths[i]))).join(' ');
186
+ const separator = widths.map((w) => chalk.dim('─'.repeat(w))).join(chalk.dim('──'));
187
+
188
+ console.log();
189
+ console.log(` ${header}`);
190
+ console.log(` ${separator}`);
191
+
192
+ for (const row of rows) {
193
+ const line = columns
194
+ .map((col, i) => {
195
+ const val = formatCell(row[col]);
196
+ return typeof row[col] === 'number'
197
+ ? chalk.hex('#f59e0b')(val.padStart(widths[i]))
198
+ : val.padEnd(widths[i]);
199
+ })
200
+ .join(' ');
201
+ console.log(` ${line}`);
202
+ }
203
+
204
+ console.log();
205
+ console.log(chalk.dim(` ${rows.length} row(s)`));
206
+ console.log();
207
+ }
208
+
209
+ function formatCell(value: unknown): string {
210
+ if (value == null) return '';
211
+ if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
212
+ return String(value);
213
+ }
214
+
215
+ async function fetchAndPrint(serverUrl: string, path: string, key: string | null) {
216
+ const res = await fetch(`${serverUrl}${path}`);
217
+ const json = await res.json();
218
+ const data = key ? (json as Record<string, unknown>)[key] : json;
219
+ console.log();
220
+ console.log(JSON.stringify(data, null, 2));
221
+ console.log();
222
+ }
@@ -0,0 +1,122 @@
1
+ import { writeFile, mkdir, readFile, unlink } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { spawn } from 'node:child_process';
5
+ import { loadConfig } from '../config/loader.js';
6
+ import { createServer } from '../server/app.js';
7
+ import { log } from '../utils/logger.js';
8
+ import { printCompactBanner, printServerStartup } from '../utils/banner.js';
9
+
10
+ export async function serveCommand(opts: { port?: number; host?: string; daemon?: boolean; stop?: boolean }) {
11
+ const cwd = process.cwd();
12
+ const pqDir = resolve(cwd, '.pipequery');
13
+ const serverJsonPath = resolve(pqDir, 'server.json');
14
+
15
+ // Handle --stop flag
16
+ if (opts.stop) {
17
+ return stopDaemon(serverJsonPath);
18
+ }
19
+
20
+ const { config, path: configPath } = await loadConfig(cwd);
21
+ const port = opts.port ?? config.server.port;
22
+ const host = opts.host ?? config.server.host;
23
+
24
+ // Daemon mode: fork a detached child process
25
+ if (opts.daemon) {
26
+ return startDaemon(cwd, port, host);
27
+ }
28
+
29
+ // Foreground mode
30
+ const { app, sourceManager } = await createServer(config, configPath, cwd);
31
+
32
+ await mkdir(pqDir, { recursive: true });
33
+ const logPath = resolve(pqDir, 'server.log');
34
+ await writeFile(serverJsonPath, JSON.stringify({ url: `http://localhost:${port}`, pid: process.pid, daemon: false, log: logPath }));
35
+
36
+ await app.listen({ port, host });
37
+
38
+ const sourceCount = sourceManager.getSourceNames().length;
39
+ const endpointCount = Object.keys(config.endpoints).length;
40
+ const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
41
+
42
+ printCompactBanner();
43
+ printServerStartup({ url, sourceCount, endpointCount });
44
+
45
+ // Graceful shutdown
46
+ const shutdown = async () => {
47
+ log.dim('\nShutting down...');
48
+ await sourceManager.dispose();
49
+ await app.close();
50
+ try { await unlink(serverJsonPath); } catch {}
51
+ process.exit(0);
52
+ };
53
+
54
+ process.on('SIGINT', shutdown);
55
+ process.on('SIGTERM', shutdown);
56
+ }
57
+
58
+ async function startDaemon(cwd: string, port: number, host: string) {
59
+ const pqDir = resolve(cwd, '.pipequery');
60
+ const serverJsonPath = resolve(pqDir, 'server.json');
61
+ const logPath = resolve(pqDir, 'server.log');
62
+
63
+ // Check if already running
64
+ if (existsSync(serverJsonPath)) {
65
+ try {
66
+ const info = JSON.parse(await readFile(serverJsonPath, 'utf-8')) as { url: string; pid: number };
67
+ const res = await fetch(`${info.url}/health`).catch(() => null);
68
+ if (res?.ok) {
69
+ log.warn(`Server already running at ${info.url} (PID: ${info.pid})`);
70
+ return;
71
+ }
72
+ } catch {
73
+ // Stale server.json, proceed
74
+ }
75
+ }
76
+
77
+ await mkdir(pqDir, { recursive: true });
78
+
79
+ // Find the current script to re-run it in foreground mode
80
+ const scriptPath = process.argv[1];
81
+ const args = ['serve', '--port', String(port), '--host', host];
82
+
83
+ const out = await import('node:fs').then(fs => fs.openSync(logPath, 'a'));
84
+ const child = spawn(process.execPath, [scriptPath, ...args], {
85
+ cwd,
86
+ detached: true,
87
+ stdio: ['ignore', out, out],
88
+ env: { ...process.env, PQ_DAEMON: '1' },
89
+ });
90
+
91
+ child.unref();
92
+
93
+ // Write server info with daemon PID
94
+ await writeFile(serverJsonPath, JSON.stringify({
95
+ url: `http://localhost:${port}`,
96
+ pid: child.pid,
97
+ daemon: true,
98
+ log: logPath,
99
+ }));
100
+
101
+ log.success(`PipeQuery server started in background (PID: ${child.pid})`);
102
+ log.dim(`Logs: ${logPath}`);
103
+ log.dim(`Stop with: pq serve --stop`);
104
+ }
105
+
106
+ async function stopDaemon(serverJsonPath: string) {
107
+ if (!existsSync(serverJsonPath)) {
108
+ log.error('No running server found.');
109
+ return;
110
+ }
111
+
112
+ try {
113
+ const info = JSON.parse(await readFile(serverJsonPath, 'utf-8')) as { pid: number; url: string };
114
+ process.kill(info.pid, 'SIGTERM');
115
+ await unlink(serverJsonPath);
116
+ log.success(`Server stopped (PID: ${info.pid})`);
117
+ } catch (err) {
118
+ log.error(`Failed to stop server: ${err instanceof Error ? err.message : err}`);
119
+ // Clean up stale file
120
+ try { await unlink(serverJsonPath); } catch {}
121
+ }
122
+ }
@@ -0,0 +1,150 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, saveConfig } from '../config/loader.js';
3
+ import { getServerUrl } from '../utils/server-discovery.js';
4
+ import { log } from '../utils/logger.js';
5
+ import type { SourceConfig } from '../config/schema.js';
6
+
7
+ export async function sourceListCommand() {
8
+ const cwd = process.cwd();
9
+
10
+ // Try live server first, fall back to config
11
+ try {
12
+ const serverUrl = await getServerUrl();
13
+ const res = await fetch(`${serverUrl}/api/_control/sources`);
14
+ const statuses = await res.json() as Record<string, { healthy: boolean; rowCount: number; lastFetch: string | null; error?: string }>;
15
+
16
+ if (Object.keys(statuses).length === 0) {
17
+ log.dim('No sources configured. Add one with: pq source add <name>');
18
+ return;
19
+ }
20
+
21
+ console.log(chalk.bold('Sources:\n'));
22
+ for (const [name, status] of Object.entries(statuses)) {
23
+ const icon = status.healthy ? chalk.green('●') : chalk.red('●');
24
+ const rows = chalk.dim(`${status.rowCount} rows`);
25
+ const err = status.error ? chalk.red(` (${status.error})`) : '';
26
+ console.log(` ${icon} ${chalk.bold(name)} ${rows}${err}`);
27
+ }
28
+ } catch {
29
+ // Server not running — read from config
30
+ const { config } = await loadConfig(cwd);
31
+ const sources = Object.entries(config.sources);
32
+
33
+ if (sources.length === 0) {
34
+ log.dim('No sources configured. Add one with: pq source add <name>');
35
+ return;
36
+ }
37
+
38
+ console.log(chalk.bold('Sources (server not running):\n'));
39
+ for (const [name, src] of sources) {
40
+ const type = chalk.cyan(src.type);
41
+ const detail = 'url' in src ? chalk.dim(src.url) : 'path' in src ? chalk.dim(src.path) : '';
42
+ console.log(` ○ ${chalk.bold(name)} ${type} ${detail}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ export async function sourceAddCommand(name: string, opts: {
48
+ type: string;
49
+ url?: string;
50
+ interval?: string;
51
+ dataPath?: string;
52
+ path?: string;
53
+ watch?: boolean;
54
+ }) {
55
+ const cwd = process.cwd();
56
+ const { config, path: configPath } = await loadConfig(cwd);
57
+
58
+ let sourceConfig: SourceConfig;
59
+
60
+ switch (opts.type) {
61
+ case 'rest':
62
+ if (!opts.url) throw new Error('--url is required for REST sources');
63
+ sourceConfig = {
64
+ type: 'rest',
65
+ url: opts.url,
66
+ interval: opts.interval ?? '30s',
67
+ ...(opts.dataPath ? { dataPath: opts.dataPath } : {}),
68
+ };
69
+ break;
70
+ case 'websocket':
71
+ if (!opts.url) throw new Error('--url is required for WebSocket sources');
72
+ sourceConfig = { type: 'websocket', url: opts.url };
73
+ break;
74
+ case 'file':
75
+ if (!opts.path) throw new Error('--path is required for file sources');
76
+ sourceConfig = {
77
+ type: 'file',
78
+ path: opts.path,
79
+ ...(opts.watch ? { watch: true } : {}),
80
+ };
81
+ break;
82
+ default:
83
+ throw new Error(`Unknown source type: ${opts.type}. Use: rest, websocket, file`);
84
+ }
85
+
86
+ // Save to config
87
+ config.sources[name] = sourceConfig;
88
+ await saveConfig(config, configPath);
89
+
90
+ // If server is running, hot-add the source
91
+ try {
92
+ const serverUrl = await getServerUrl();
93
+ const res = await fetch(`${serverUrl}/api/_control/sources`, {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ name, config: sourceConfig }),
97
+ });
98
+ const json = await res.json() as { ok: boolean; status?: { rowCount: number } };
99
+ if (json.ok) {
100
+ log.success(`Source "${name}" added and live (${json.status?.rowCount ?? 0} rows)`);
101
+ return;
102
+ }
103
+ } catch {
104
+ // Server not running
105
+ }
106
+
107
+ log.success(`Source "${name}" added to config. Start server with: pq serve`);
108
+ }
109
+
110
+ export async function sourceRemoveCommand(name: string) {
111
+ const cwd = process.cwd();
112
+ const { config, path: configPath } = await loadConfig(cwd);
113
+
114
+ if (!config.sources[name]) {
115
+ log.error(`Source "${name}" not found`);
116
+ return;
117
+ }
118
+
119
+ delete config.sources[name];
120
+ await saveConfig(config, configPath);
121
+
122
+ // If server is running, hot-remove
123
+ try {
124
+ const serverUrl = await getServerUrl();
125
+ await fetch(`${serverUrl}/api/_control/sources/${encodeURIComponent(name)}`, { method: 'DELETE' });
126
+ log.success(`Source "${name}" removed (live)`);
127
+ return;
128
+ } catch {
129
+ // Server not running
130
+ }
131
+
132
+ log.success(`Source "${name}" removed from config`);
133
+ }
134
+
135
+ export async function sourceTestCommand(name: string) {
136
+ const serverUrl = await getServerUrl();
137
+ const res = await fetch(`${serverUrl}/api/_control/sources/${encodeURIComponent(name)}/test`, { method: 'POST' });
138
+ const json = await res.json() as { ok: boolean; rowCount?: number; sample?: unknown[]; error?: string };
139
+
140
+ if (!json.ok) {
141
+ log.error(json.error ?? 'Test failed');
142
+ return;
143
+ }
144
+
145
+ log.success(`Source "${name}": ${json.rowCount} rows`);
146
+ if (json.sample && json.sample.length > 0) {
147
+ console.log(chalk.dim('\nSample:'));
148
+ console.log(JSON.stringify(json.sample, null, 2));
149
+ }
150
+ }
@@ -0,0 +1,50 @@
1
+ import { readFile, unlink } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { log } from '../utils/logger.js';
5
+
6
+ export async function stopCommand(opts: { force?: boolean }) {
7
+ const cwd = process.cwd();
8
+ const serverJsonPath = resolve(cwd, '.pipequery', 'server.json');
9
+
10
+ if (!existsSync(serverJsonPath)) {
11
+ log.error('No running server found.');
12
+ log.dim('Start one with: pq serve or pq serve -d');
13
+ return;
14
+ }
15
+
16
+ let info: { pid: number; url: string; daemon?: boolean };
17
+ try {
18
+ info = JSON.parse(await readFile(serverJsonPath, 'utf-8'));
19
+ } catch {
20
+ log.error('Could not read server info. Cleaning up stale file.');
21
+ try { await unlink(serverJsonPath); } catch {}
22
+ return;
23
+ }
24
+
25
+ // Check if process is actually running
26
+ let alive = false;
27
+ try {
28
+ process.kill(info.pid, 0); // signal 0 = just check existence
29
+ alive = true;
30
+ } catch {
31
+ // Process not running
32
+ }
33
+
34
+ if (!alive) {
35
+ log.warn(`Server (PID: ${info.pid}) is not running. Cleaning up stale state.`);
36
+ try { await unlink(serverJsonPath); } catch {}
37
+ return;
38
+ }
39
+
40
+ // Send signal
41
+ const signal = opts.force ? 'SIGKILL' : 'SIGTERM';
42
+ try {
43
+ process.kill(info.pid, signal);
44
+ log.success(`Server stopped (PID: ${info.pid})${info.daemon ? ' [daemon]' : ''}`);
45
+ } catch (err) {
46
+ log.error(`Failed to stop server: ${err instanceof Error ? err.message : err}`);
47
+ }
48
+
49
+ try { await unlink(serverJsonPath); } catch {}
50
+ }
@@ -0,0 +1,48 @@
1
+ import type { PipeQueryConfig } from './schema.js';
2
+
3
+ export const DEFAULT_CONFIG: PipeQueryConfig = {
4
+ server: {
5
+ port: 3000,
6
+ host: '0.0.0.0',
7
+ },
8
+ sources: {},
9
+ endpoints: {},
10
+ dashboards: {},
11
+ };
12
+
13
+ export const STARTER_YAML = `# PipeQuery Configuration
14
+ # Docs: https://github.com/andreadito/pipequery
15
+
16
+ server:
17
+ port: 3000
18
+ host: 0.0.0.0
19
+
20
+ # Data sources — REST APIs, WebSockets, files, or inline JSON
21
+ sources:
22
+ # Example: crypto prices from CoinCap
23
+ # crypto:
24
+ # type: rest
25
+ # url: https://api.coincap.io/v2/assets
26
+ # dataPath: data
27
+ # interval: 30s
28
+ #
29
+ # orders:
30
+ # type: file
31
+ # path: ./data/orders.csv
32
+ # watch: true
33
+
34
+ # API endpoints — expose PipeQuery results as REST APIs
35
+ endpoints: {}
36
+ # /api/top-coins:
37
+ # query: "crypto | sort(market_cap desc) | first(10)"
38
+ # cache: 30s
39
+
40
+ # Terminal dashboards
41
+ dashboards: {}
42
+ # main:
43
+ # refresh: 10s
44
+ # panels:
45
+ # - title: Top Assets
46
+ # query: "crypto | sort(market_cap desc) | first(10)"
47
+ # viz: table
48
+ `;
@@ -0,0 +1,37 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { readYaml, writeYaml } from '../utils/yaml.js';
4
+ import { DEFAULT_CONFIG } from './defaults.js';
5
+ import type { PipeQueryConfig } from './schema.js';
6
+
7
+ const CONFIG_NAMES = ['pipequery.yaml', 'pipequery.yml'];
8
+
9
+ export function findConfigPath(cwd = process.cwd()): string | null {
10
+ for (const name of CONFIG_NAMES) {
11
+ const p = resolve(cwd, name);
12
+ if (existsSync(p)) return p;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export async function loadConfig(cwd = process.cwd()): Promise<{ config: PipeQueryConfig; path: string }> {
18
+ const configPath = findConfigPath(cwd);
19
+ if (!configPath) {
20
+ throw new Error('No pipequery.yaml found. Run `pq init` to create one.');
21
+ }
22
+
23
+ const raw = await readYaml<Partial<PipeQueryConfig>>(configPath);
24
+ const config: PipeQueryConfig = {
25
+ server: { ...DEFAULT_CONFIG.server, ...raw.server },
26
+ remote: raw.remote,
27
+ sources: raw.sources ?? {},
28
+ endpoints: raw.endpoints ?? {},
29
+ dashboards: raw.dashboards ?? {},
30
+ };
31
+
32
+ return { config, path: configPath };
33
+ }
34
+
35
+ export async function saveConfig(config: PipeQueryConfig, configPath: string): Promise<void> {
36
+ await writeYaml(configPath, config);
37
+ }
@@ -0,0 +1,73 @@
1
+ export interface PipeQueryConfig {
2
+ server: ServerConfig;
3
+ remote?: RemoteConfig;
4
+ sources: Record<string, SourceConfig>;
5
+ endpoints: Record<string, EndpointConfig>;
6
+ dashboards: Record<string, DashboardConfig>;
7
+ }
8
+
9
+ export interface ServerConfig {
10
+ port: number;
11
+ host: string;
12
+ }
13
+
14
+ export interface RemoteConfig {
15
+ url: string;
16
+ }
17
+
18
+ // ─── Source configs ──────────────────────────────────────────────────────────
19
+
20
+ export type SourceConfig =
21
+ | RestSourceConfig
22
+ | WebSocketSourceConfig
23
+ | FileSourceConfig
24
+ | StaticSourceConfig;
25
+
26
+ export interface RestSourceConfig {
27
+ type: 'rest';
28
+ url: string;
29
+ params?: Record<string, string>;
30
+ headers?: Record<string, string>;
31
+ dataPath?: string;
32
+ interval?: string;
33
+ }
34
+
35
+ export interface WebSocketSourceConfig {
36
+ type: 'websocket';
37
+ url: string;
38
+ maxBuffer?: number;
39
+ }
40
+
41
+ export interface FileSourceConfig {
42
+ type: 'file';
43
+ path: string;
44
+ format?: 'json' | 'csv';
45
+ watch?: boolean;
46
+ }
47
+
48
+ export interface StaticSourceConfig {
49
+ type: 'static';
50
+ data: unknown[];
51
+ }
52
+
53
+ // ─── Endpoint config ─────────────────────────────────────────────────────────
54
+
55
+ export interface EndpointConfig {
56
+ query: string;
57
+ cache?: string;
58
+ method?: string;
59
+ }
60
+
61
+ // ─── Dashboard config ────────────────────────────────────────────────────────
62
+
63
+ export interface DashboardConfig {
64
+ refresh?: string;
65
+ panels: PanelConfig[];
66
+ }
67
+
68
+ export interface PanelConfig {
69
+ title: string;
70
+ query: string;
71
+ viz: 'table' | 'bar' | 'sparkline' | 'stat' | 'orderbook' | 'heatmap' | 'candle' | 'auto';
72
+ size?: 'full' | 'half' | 'stat';
73
+ }