@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
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vaultgradient/pipequery-cli",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "CLI for PipeQuery — data sources, aggregations, APIs, and terminal dashboards",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pq": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@fastify/cors": "^11.2.0",
|
|
17
|
+
"chalk": "^5.6.2",
|
|
18
|
+
"chokidar": "^5.0.0",
|
|
19
|
+
"commander": "^14.0.3",
|
|
20
|
+
"csv-parse": "^6.2.1",
|
|
21
|
+
"fastify": "^5.8.5",
|
|
22
|
+
"ink": "^7.0.1",
|
|
23
|
+
"ora": "^9.3.0",
|
|
24
|
+
"react": "^19.2.5",
|
|
25
|
+
"ws": "^8.20.0",
|
|
26
|
+
"yaml": "^2.8.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.6.0",
|
|
30
|
+
"@types/react": "^19.2.14",
|
|
31
|
+
"@types/ws": "^8.18.1",
|
|
32
|
+
"tsup": "^8.5.1",
|
|
33
|
+
"typescript": "~6.0.0",
|
|
34
|
+
"vitest": "^4.1.4"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/andreadito/pipequery.git",
|
|
39
|
+
"directory": "cli"
|
|
40
|
+
},
|
|
41
|
+
"author": "andreadito",
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { log } from '../utils/logger.js';
|
|
2
|
+
|
|
3
|
+
const BASH_COMPLETION = `###-begin-pq-completions-###
|
|
4
|
+
_pq_completions() {
|
|
5
|
+
local cur prev commands source_cmds endpoint_cmds remote_cmds
|
|
6
|
+
COMPREPLY=()
|
|
7
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
8
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
+
|
|
10
|
+
commands="init serve query source endpoint dashboard remote help"
|
|
11
|
+
source_cmds="list add remove test"
|
|
12
|
+
endpoint_cmds="list add remove"
|
|
13
|
+
remote_cmds="deploy connect status"
|
|
14
|
+
|
|
15
|
+
case "\${COMP_WORDS[1]}" in
|
|
16
|
+
source)
|
|
17
|
+
COMPREPLY=( $(compgen -W "$source_cmds" -- "$cur") )
|
|
18
|
+
return 0
|
|
19
|
+
;;
|
|
20
|
+
endpoint)
|
|
21
|
+
COMPREPLY=( $(compgen -W "$endpoint_cmds" -- "$cur") )
|
|
22
|
+
return 0
|
|
23
|
+
;;
|
|
24
|
+
remote)
|
|
25
|
+
COMPREPLY=( $(compgen -W "$remote_cmds" -- "$cur") )
|
|
26
|
+
return 0
|
|
27
|
+
;;
|
|
28
|
+
serve)
|
|
29
|
+
COMPREPLY=( $(compgen -W "--port --host --daemon --stop" -- "$cur") )
|
|
30
|
+
return 0
|
|
31
|
+
;;
|
|
32
|
+
query)
|
|
33
|
+
COMPREPLY=( $(compgen -W "--format" -- "$cur") )
|
|
34
|
+
return 0
|
|
35
|
+
;;
|
|
36
|
+
dashboard)
|
|
37
|
+
COMPREPLY=( $(compgen -W "--name" -- "$cur") )
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
|
|
42
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
43
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
complete -F _pq_completions pq
|
|
49
|
+
###-end-pq-completions-###`;
|
|
50
|
+
|
|
51
|
+
const ZSH_COMPLETION = `###-begin-pq-completions-###
|
|
52
|
+
_pq() {
|
|
53
|
+
local -a commands source_cmds endpoint_cmds remote_cmds
|
|
54
|
+
|
|
55
|
+
commands=(
|
|
56
|
+
'init:Create a pipequery.yaml config file'
|
|
57
|
+
'serve:Start the PipeQuery server'
|
|
58
|
+
'query:Run an ad-hoc PipeQuery expression'
|
|
59
|
+
'source:Manage data sources'
|
|
60
|
+
'endpoint:Manage API endpoints'
|
|
61
|
+
'dashboard:Launch the interactive terminal dashboard'
|
|
62
|
+
'remote:Manage remote deployment'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
source_cmds=(
|
|
66
|
+
'list:List configured data sources'
|
|
67
|
+
'add:Add a data source'
|
|
68
|
+
'remove:Remove a data source'
|
|
69
|
+
'test:Test a data source'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
endpoint_cmds=(
|
|
73
|
+
'list:List API endpoints'
|
|
74
|
+
'add:Register an API endpoint'
|
|
75
|
+
'remove:Remove an API endpoint'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
remote_cmds=(
|
|
79
|
+
'deploy:Generate Dockerfile and docker-compose.yaml'
|
|
80
|
+
'connect:Connect CLI to a remote PipeQuery server'
|
|
81
|
+
'status:Check remote server health'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_arguments -C \\
|
|
85
|
+
'1: :->command' \\
|
|
86
|
+
'2: :->subcommand' \\
|
|
87
|
+
'*::arg:->args'
|
|
88
|
+
|
|
89
|
+
case $state in
|
|
90
|
+
command)
|
|
91
|
+
_describe 'command' commands
|
|
92
|
+
;;
|
|
93
|
+
subcommand)
|
|
94
|
+
case $words[2] in
|
|
95
|
+
source) _describe 'subcommand' source_cmds ;;
|
|
96
|
+
endpoint) _describe 'subcommand' endpoint_cmds ;;
|
|
97
|
+
remote) _describe 'subcommand' remote_cmds ;;
|
|
98
|
+
esac
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
compdef _pq pq
|
|
103
|
+
###-end-pq-completions-###`;
|
|
104
|
+
|
|
105
|
+
const FISH_COMPLETION = `###-begin-pq-completions-###
|
|
106
|
+
# Top-level commands
|
|
107
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'init' -d 'Create a pipequery.yaml config file'
|
|
108
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'serve' -d 'Start the PipeQuery server'
|
|
109
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'query' -d 'Run an ad-hoc PipeQuery expression'
|
|
110
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'source' -d 'Manage data sources'
|
|
111
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'endpoint' -d 'Manage API endpoints'
|
|
112
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'dashboard' -d 'Launch the terminal dashboard'
|
|
113
|
+
complete -c pq -f -n '__fish_use_subcommand' -a 'remote' -d 'Manage remote deployment'
|
|
114
|
+
|
|
115
|
+
# source subcommands
|
|
116
|
+
complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'list' -d 'List data sources'
|
|
117
|
+
complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'add' -d 'Add a data source'
|
|
118
|
+
complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'remove' -d 'Remove a data source'
|
|
119
|
+
complete -c pq -f -n '__fish_seen_subcommand_from source' -a 'test' -d 'Test a data source'
|
|
120
|
+
|
|
121
|
+
# endpoint subcommands
|
|
122
|
+
complete -c pq -f -n '__fish_seen_subcommand_from endpoint' -a 'list' -d 'List endpoints'
|
|
123
|
+
complete -c pq -f -n '__fish_seen_subcommand_from endpoint' -a 'add' -d 'Add an endpoint'
|
|
124
|
+
complete -c pq -f -n '__fish_seen_subcommand_from endpoint' -a 'remove' -d 'Remove an endpoint'
|
|
125
|
+
|
|
126
|
+
# remote subcommands
|
|
127
|
+
complete -c pq -f -n '__fish_seen_subcommand_from remote' -a 'deploy' -d 'Generate Docker files'
|
|
128
|
+
complete -c pq -f -n '__fish_seen_subcommand_from remote' -a 'connect' -d 'Connect to remote server'
|
|
129
|
+
complete -c pq -f -n '__fish_seen_subcommand_from remote' -a 'status' -d 'Check remote health'
|
|
130
|
+
|
|
131
|
+
# serve flags
|
|
132
|
+
complete -c pq -l port -s p -n '__fish_seen_subcommand_from serve' -d 'Server port'
|
|
133
|
+
complete -c pq -l host -s H -n '__fish_seen_subcommand_from serve' -d 'Server host'
|
|
134
|
+
complete -c pq -l daemon -s d -n '__fish_seen_subcommand_from serve' -d 'Run as daemon'
|
|
135
|
+
complete -c pq -l stop -n '__fish_seen_subcommand_from serve' -d 'Stop running daemon'
|
|
136
|
+
###-end-pq-completions-###`;
|
|
137
|
+
|
|
138
|
+
export async function completionCommand(opts: { shell?: string }) {
|
|
139
|
+
const shell = opts.shell ?? detectShell();
|
|
140
|
+
|
|
141
|
+
switch (shell) {
|
|
142
|
+
case 'bash':
|
|
143
|
+
console.log(BASH_COMPLETION);
|
|
144
|
+
log.dim('\n# Add to ~/.bashrc:');
|
|
145
|
+
log.dim('# eval "$(pq completion --shell bash)"');
|
|
146
|
+
break;
|
|
147
|
+
case 'zsh':
|
|
148
|
+
console.log(ZSH_COMPLETION);
|
|
149
|
+
log.dim('\n# Add to ~/.zshrc:');
|
|
150
|
+
log.dim('# eval "$(pq completion --shell zsh)"');
|
|
151
|
+
break;
|
|
152
|
+
case 'fish':
|
|
153
|
+
console.log(FISH_COMPLETION);
|
|
154
|
+
log.dim('\n# Save to ~/.config/fish/completions/pq.fish:');
|
|
155
|
+
log.dim('# pq completion --shell fish > ~/.config/fish/completions/pq.fish');
|
|
156
|
+
break;
|
|
157
|
+
default:
|
|
158
|
+
log.error(`Unknown shell: ${shell}. Use --shell bash|zsh|fish`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function detectShell(): string {
|
|
163
|
+
const shell = process.env.SHELL ?? '';
|
|
164
|
+
if (shell.includes('zsh')) return 'zsh';
|
|
165
|
+
if (shell.includes('fish')) return 'fish';
|
|
166
|
+
return 'bash';
|
|
167
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { loadConfig } from '../config/loader.js';
|
|
4
|
+
import { getServerUrl } from '../utils/server-discovery.js';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
import { App } from '../tui/App.js';
|
|
7
|
+
import { enableSyncOutput, disableSyncOutput } from '../utils/sync-output.js';
|
|
8
|
+
|
|
9
|
+
export async function dashboardCommand(opts: { name?: string }) {
|
|
10
|
+
const dashboardName = opts.name ?? 'main';
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const { config } = await loadConfig(cwd);
|
|
13
|
+
|
|
14
|
+
const dashboard = config.dashboards[dashboardName];
|
|
15
|
+
if (!dashboard) {
|
|
16
|
+
log.error(`Dashboard "${dashboardName}" not found in config.`);
|
|
17
|
+
const available = Object.keys(config.dashboards);
|
|
18
|
+
if (available.length > 0) {
|
|
19
|
+
log.info(`Available dashboards: ${available.join(', ')}`);
|
|
20
|
+
} else {
|
|
21
|
+
log.dim('Add a dashboard section to pipequery.yaml');
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const serverUrl = await getServerUrl();
|
|
27
|
+
|
|
28
|
+
// Enter alternate screen buffer + synchronized output
|
|
29
|
+
process.stdout.write('\x1B[?1049h');
|
|
30
|
+
process.stdout.write('\x1B[?25l');
|
|
31
|
+
enableSyncOutput();
|
|
32
|
+
|
|
33
|
+
const { waitUntilExit } = render(
|
|
34
|
+
React.createElement(App, { serverUrl, dashboard }),
|
|
35
|
+
{ patchConsole: false },
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await waitUntilExit();
|
|
39
|
+
|
|
40
|
+
// Restore
|
|
41
|
+
disableSyncOutput();
|
|
42
|
+
process.stdout.write('\x1B[?25h');
|
|
43
|
+
process.stdout.write('\x1B[?1049l');
|
|
44
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
|
|
6
|
+
export async function endpointListCommand() {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const serverUrl = await getServerUrl();
|
|
11
|
+
const res = await fetch(`${serverUrl}/api/_control/endpoints`);
|
|
12
|
+
const endpoints = await res.json() as Record<string, { query: string; cache?: string }>;
|
|
13
|
+
|
|
14
|
+
if (Object.keys(endpoints).length === 0) {
|
|
15
|
+
log.dim('No endpoints configured. Add one with: pq endpoint add <path> --query "<expr>"');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.bold('Endpoints:\n'));
|
|
20
|
+
for (const [path, config] of Object.entries(endpoints)) {
|
|
21
|
+
const cache = config.cache ? chalk.dim(`(cache: ${config.cache})`) : '';
|
|
22
|
+
console.log(` ${chalk.green('GET')} ${chalk.bold(path)} ${cache}`);
|
|
23
|
+
console.log(` ${chalk.dim(config.query)}`);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
const { config } = await loadConfig(cwd);
|
|
27
|
+
const entries = Object.entries(config.endpoints);
|
|
28
|
+
|
|
29
|
+
if (entries.length === 0) {
|
|
30
|
+
log.dim('No endpoints configured. Add one with: pq endpoint add <path> --query "<expr>"');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(chalk.bold('Endpoints (server not running):\n'));
|
|
35
|
+
for (const [path, cfg] of entries) {
|
|
36
|
+
console.log(` ${chalk.green('GET')} ${chalk.bold(path)}`);
|
|
37
|
+
console.log(` ${chalk.dim(cfg.query)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function endpointAddCommand(path: string, opts: { query: string; cache?: string }) {
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const { config, path: configPath } = await loadConfig(cwd);
|
|
45
|
+
|
|
46
|
+
// Ensure path starts with /
|
|
47
|
+
const apiPath = path.startsWith('/') ? path : `/${path}`;
|
|
48
|
+
|
|
49
|
+
const endpointConfig = {
|
|
50
|
+
query: opts.query,
|
|
51
|
+
...(opts.cache ? { cache: opts.cache } : {}),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Save to config
|
|
55
|
+
config.endpoints[apiPath] = endpointConfig;
|
|
56
|
+
await saveConfig(config, configPath);
|
|
57
|
+
|
|
58
|
+
// Hot-add if server running
|
|
59
|
+
try {
|
|
60
|
+
const serverUrl = await getServerUrl();
|
|
61
|
+
await fetch(`${serverUrl}/api/_control/endpoints`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ path: apiPath, config: endpointConfig }),
|
|
65
|
+
});
|
|
66
|
+
log.success(`Endpoint ${apiPath} registered (live)`);
|
|
67
|
+
return;
|
|
68
|
+
} catch {
|
|
69
|
+
// Server not running
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
log.success(`Endpoint ${apiPath} added to config. Start server with: pq serve`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function endpointRemoveCommand(path: string) {
|
|
76
|
+
const cwd = process.cwd();
|
|
77
|
+
const { config, path: configPath } = await loadConfig(cwd);
|
|
78
|
+
|
|
79
|
+
const apiPath = path.startsWith('/') ? path : `/${path}`;
|
|
80
|
+
|
|
81
|
+
if (!config.endpoints[apiPath]) {
|
|
82
|
+
log.error(`Endpoint "${apiPath}" not found`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
delete config.endpoints[apiPath];
|
|
87
|
+
await saveConfig(config, configPath);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const serverUrl = await getServerUrl();
|
|
91
|
+
await fetch(`${serverUrl}/api/_control/endpoints/${apiPath.slice(1)}`, { method: 'DELETE' });
|
|
92
|
+
log.success(`Endpoint ${apiPath} removed (live)`);
|
|
93
|
+
return;
|
|
94
|
+
} catch {
|
|
95
|
+
// Server not running
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log.success(`Endpoint ${apiPath} removed from config`);
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { log } from '../utils/logger.js';
|
|
5
|
+
import { STARTER_YAML } from '../config/defaults.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
export async function initCommand() {
|
|
9
|
+
const target = resolve(process.cwd(), 'pipequery.yaml');
|
|
10
|
+
|
|
11
|
+
if (existsSync(target)) {
|
|
12
|
+
log.warn('pipequery.yaml already exists in this directory.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await writeFile(target, STARTER_YAML, 'utf-8');
|
|
17
|
+
log.success('Created pipequery.yaml');
|
|
18
|
+
log.step(`Edit ${chalk.white.bold('pipequery.yaml')} to configure your data sources`);
|
|
19
|
+
log.step(`Then run ${chalk.white.bold('pq serve')} to start the server`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { getServerUrl } from '../utils/server-discovery.js';
|
|
6
|
+
import { log } from '../utils/logger.js';
|
|
7
|
+
import { MonitorApp } from '../tui/MonitorApp.js';
|
|
8
|
+
import { enableSyncOutput, disableSyncOutput } from '../utils/sync-output.js';
|
|
9
|
+
|
|
10
|
+
export async function monitorCommand() {
|
|
11
|
+
const serverUrl = await getServerUrl();
|
|
12
|
+
|
|
13
|
+
// Try to find log path from server discovery
|
|
14
|
+
const discoveryPath = resolve(process.cwd(), '.pipequery', 'server.json');
|
|
15
|
+
let logPath: string | undefined;
|
|
16
|
+
|
|
17
|
+
if (existsSync(discoveryPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const disc = JSON.parse(readFileSync(discoveryPath, 'utf-8')) as { log?: string };
|
|
20
|
+
if (disc.log && existsSync(disc.log)) {
|
|
21
|
+
logPath = disc.log;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fallback log path
|
|
29
|
+
if (!logPath) {
|
|
30
|
+
const fallback = resolve(process.cwd(), '.pipequery', 'server.log');
|
|
31
|
+
if (existsSync(fallback)) {
|
|
32
|
+
logPath = fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Enter alternate screen buffer + synchronized output
|
|
37
|
+
process.stdout.write('\x1B[?1049h');
|
|
38
|
+
process.stdout.write('\x1B[?25l');
|
|
39
|
+
enableSyncOutput();
|
|
40
|
+
|
|
41
|
+
const { waitUntilExit } = render(
|
|
42
|
+
React.createElement(MonitorApp, { serverUrl, logPath }),
|
|
43
|
+
{ patchConsole: false },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await waitUntilExit();
|
|
47
|
+
|
|
48
|
+
// Restore
|
|
49
|
+
disableSyncOutput();
|
|
50
|
+
process.stdout.write('\x1B[?25h');
|
|
51
|
+
process.stdout.write('\x1B[?1049l');
|
|
52
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getServerUrl } from '../utils/server-discovery.js';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
export async function queryCommand(expression: string, opts: { format?: string }) {
|
|
6
|
+
const serverUrl = await getServerUrl();
|
|
7
|
+
|
|
8
|
+
const res = await fetch(`${serverUrl}/api/_control/query`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11
|
+
body: JSON.stringify({ query: expression }),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const json = await res.json() as { ok: boolean; result?: unknown; error?: string };
|
|
15
|
+
if (!json.ok) {
|
|
16
|
+
log.error(json.error ?? 'Query failed');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = json.result;
|
|
21
|
+
|
|
22
|
+
if (opts.format === 'json') {
|
|
23
|
+
console.log(JSON.stringify(result, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Render as table
|
|
28
|
+
if (Array.isArray(result) && result.length > 0 && typeof result[0] === 'object') {
|
|
29
|
+
renderTable(result as Record<string, unknown>[]);
|
|
30
|
+
} else if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
|
|
31
|
+
renderTable([result as Record<string, unknown>]);
|
|
32
|
+
} else {
|
|
33
|
+
console.log(result);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderTable(rows: Record<string, unknown>[]) {
|
|
38
|
+
if (rows.length === 0) {
|
|
39
|
+
log.dim('(empty result)');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const columns = Object.keys(rows[0]);
|
|
44
|
+
const widths = columns.map((col) => {
|
|
45
|
+
const maxData = rows.reduce((max, row) => Math.max(max, formatCell(row[col]).length), 0);
|
|
46
|
+
return Math.max(col.length, maxData);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Header
|
|
50
|
+
const header = columns.map((col, i) => chalk.hex('#06b6d4').bold(col.padEnd(widths[i]))).join(' ');
|
|
51
|
+
const separator = widths.map((w) => chalk.dim('─'.repeat(w))).join(chalk.dim('──'));
|
|
52
|
+
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(` ${header}`);
|
|
55
|
+
console.log(` ${separator}`);
|
|
56
|
+
|
|
57
|
+
// Rows
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const line = columns
|
|
60
|
+
.map((col, i) => {
|
|
61
|
+
const val = formatCell(row[col]);
|
|
62
|
+
return typeof row[col] === 'number'
|
|
63
|
+
? chalk.hex('#f59e0b')(val.padStart(widths[i]))
|
|
64
|
+
: val.padEnd(widths[i]);
|
|
65
|
+
})
|
|
66
|
+
.join(' ');
|
|
67
|
+
console.log(` ${line}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.dim(` ${rows.length} row(s)`));
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatCell(value: unknown): string {
|
|
76
|
+
if (value == null) return '';
|
|
77
|
+
if (typeof value === 'number') {
|
|
78
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
79
|
+
}
|
|
80
|
+
return String(value);
|
|
81
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { findConfigPath, loadConfig, saveConfig } from '../config/loader.js';
|
|
4
|
+
import { DEFAULT_CONFIG } from '../config/defaults.js';
|
|
5
|
+
import type { PipeQueryConfig } from '../config/schema.js';
|
|
6
|
+
import { log } from '../utils/logger.js';
|
|
7
|
+
import { generateDockerfile, generateDockerCompose, generateDockerignore } from '../docker/templates.js';
|
|
8
|
+
|
|
9
|
+
export async function remoteDeployCommand() {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const { config } = await loadConfig(cwd);
|
|
12
|
+
const port = config.server.port;
|
|
13
|
+
|
|
14
|
+
// Generate files
|
|
15
|
+
await writeFile(resolve(cwd, 'Dockerfile'), generateDockerfile(port));
|
|
16
|
+
await writeFile(resolve(cwd, 'docker-compose.yaml'), generateDockerCompose(port));
|
|
17
|
+
await writeFile(resolve(cwd, '.dockerignore'), generateDockerignore());
|
|
18
|
+
|
|
19
|
+
log.success('Generated Dockerfile, docker-compose.yaml, and .dockerignore');
|
|
20
|
+
log.info(`Run: docker compose up -d --build`);
|
|
21
|
+
log.info(`Then: pq remote connect http://localhost:${port}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load config if it exists, or create a minimal one for remote-only usage.
|
|
26
|
+
*/
|
|
27
|
+
async function loadOrCreateConfig(cwd: string): Promise<{ config: PipeQueryConfig; path: string }> {
|
|
28
|
+
const existing = findConfigPath(cwd);
|
|
29
|
+
if (existing) {
|
|
30
|
+
return loadConfig(cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// No local config — create a minimal one for remote connection
|
|
34
|
+
const configPath = resolve(cwd, 'pipequery.yaml');
|
|
35
|
+
const config: PipeQueryConfig = {
|
|
36
|
+
server: { ...DEFAULT_CONFIG.server },
|
|
37
|
+
sources: {},
|
|
38
|
+
endpoints: {},
|
|
39
|
+
dashboards: {},
|
|
40
|
+
};
|
|
41
|
+
await saveConfig(config, configPath);
|
|
42
|
+
return { config, path: configPath };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function remoteConnectCommand(url: string) {
|
|
46
|
+
// Verify the remote server is reachable first
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${url}/health`);
|
|
49
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log.error(`Cannot reach ${url}: ${err instanceof Error ? err.message : err}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
const { config, path: configPath } = await loadOrCreateConfig(cwd);
|
|
57
|
+
|
|
58
|
+
config.remote = { url };
|
|
59
|
+
await saveConfig(config, configPath);
|
|
60
|
+
log.success(`Connected to remote server at ${url}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function remoteStatusCommand() {
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
|
|
66
|
+
const existing = findConfigPath(cwd);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
log.error('No remote server configured. Use: pq remote connect <url>');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { config } = await loadConfig(cwd);
|
|
73
|
+
const url = config.remote?.url;
|
|
74
|
+
if (!url) {
|
|
75
|
+
log.error('No remote server configured. Use: pq remote connect <url>');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(`${url}/status`);
|
|
81
|
+
const status = await res.json() as { status: string; uptime: number; sources: Record<string, unknown> };
|
|
82
|
+
|
|
83
|
+
log.success(`Remote server at ${url}`);
|
|
84
|
+
log.info(`Uptime: ${Math.floor(status.uptime / 1000)}s`);
|
|
85
|
+
log.info(`Sources: ${Object.keys(status.sources).length}`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.error(`Cannot reach ${url}: ${err instanceof Error ? err.message : err}`);
|
|
88
|
+
}
|
|
89
|
+
}
|