@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/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
+ }