@kazurolabs/psql-explorer 0.0.0-stage → 0.3.0
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/LICENSE +21 -0
- package/README.md +505 -2
- package/RELEASES.md +87 -0
- package/dist/app.js +39 -0
- package/dist/cli.js +307 -0
- package/dist/commands/configgen.js +128 -0
- package/dist/commands/explore.js +388 -0
- package/dist/commands/terminal-prompt.js +68 -0
- package/dist/config/load.js +324 -0
- package/dist/config/resolve-environment.js +112 -0
- package/dist/config/resolvers/env.js +12 -0
- package/dist/config/resolvers/file.js +24 -0
- package/dist/config/resolvers/groups.js +1 -0
- package/dist/config/resolvers/index.js +49 -0
- package/dist/config/resolvers/literal.js +7 -0
- package/dist/config/resolvers/prompt.js +159 -0
- package/dist/config/resolvers/stored.js +13 -0
- package/dist/config/resolvers/types.js +18 -0
- package/dist/config/resolvers/vault.js +283 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/source.js +105 -0
- package/dist/db/connect.js +55 -0
- package/dist/db/identifiers.js +6 -0
- package/dist/db/paths.js +14 -0
- package/dist/db/store.js +323 -0
- package/dist/db/tester.js +190 -0
- package/dist/db/validate.js +99 -0
- package/dist/environments.js +140 -0
- package/dist/explorer/alter.js +493 -0
- package/dist/explorer/catalog.js +414 -0
- package/dist/explorer/connect-entry.js +42 -0
- package/dist/explorer/cypher-parse.js +478 -0
- package/dist/explorer/cypher.js +963 -0
- package/dist/explorer/ddl.js +502 -0
- package/dist/explorer/dependents.js +222 -0
- package/dist/explorer/design-load.js +254 -0
- package/dist/explorer/design.js +45 -0
- package/dist/explorer/edit.js +108 -0
- package/dist/explorer/explain.js +37 -0
- package/dist/explorer/filter.js +121 -0
- package/dist/explorer/format.js +134 -0
- package/dist/explorer/graph.js +679 -0
- package/dist/explorer/gucs.js +31 -0
- package/dist/explorer/profile.js +102 -0
- package/dist/explorer/references.js +95 -0
- package/dist/explorer/rows.js +64 -0
- package/dist/explorer/schema-diff.js +360 -0
- package/dist/explorer/search.js +309 -0
- package/dist/explorer/security.js +152 -0
- package/dist/explorer/seed.js +1376 -0
- package/dist/explorer/session.js +122 -0
- package/dist/explorer/stats.js +135 -0
- package/dist/explorer/timeout.js +13 -0
- package/dist/prompts.js +52 -0
- package/dist/screens/environments.js +229 -0
- package/dist/screens/explorer.js +145 -0
- package/dist/screens/header.js +8 -0
- package/dist/screens/index.js +13 -0
- package/dist/screens/new-connection.js +178 -0
- package/dist/screens/panes/common.js +35 -0
- package/dist/screens/panes/history.js +36 -0
- package/dist/screens/panes/info.js +61 -0
- package/dist/screens/panes/query.js +113 -0
- package/dist/screens/panes/table.js +178 -0
- package/dist/screens/panes/tables.js +142 -0
- package/dist/screens/prompt-modal.js +18 -0
- package/dist/screens/record-view.js +42 -0
- package/dist/screens/result-grid.js +126 -0
- package/dist/screens/scroll.js +12 -0
- package/dist/screens/splash.js +78 -0
- package/dist/screens/terminal.js +25 -0
- package/dist/screens/types.js +1 -0
- package/dist/services.js +1 -0
- package/dist/startup.js +84 -0
- package/dist/web/server.js +988 -0
- package/package.json +69 -5
- package/web/dist/assets/anton-latin-400-normal-AUNGEG_V.woff +0 -0
- package/web/dist/assets/anton-latin-400-normal-Byf51wtH.woff2 +0 -0
- package/web/dist/assets/anton-latin-ext-400-normal-BMODBQc6.woff +0 -0
- package/web/dist/assets/anton-latin-ext-400-normal-SyiqE2Jt.woff2 +0 -0
- package/web/dist/assets/anton-vietnamese-400-normal-2FfR1wHA.woff +0 -0
- package/web/dist/assets/anton-vietnamese-400-normal-CkBxLiRJ.woff2 +0 -0
- package/web/dist/assets/antonio-latin-400-normal-B7iMKlzw.woff +0 -0
- package/web/dist/assets/antonio-latin-400-normal-CZjzwNAN.woff2 +0 -0
- package/web/dist/assets/antonio-latin-700-normal-BY3mCh47.woff +0 -0
- package/web/dist/assets/antonio-latin-700-normal-DSH-lEDG.woff2 +0 -0
- package/web/dist/assets/antonio-latin-ext-400-normal-nE82dizb.woff2 +0 -0
- package/web/dist/assets/antonio-latin-ext-400-normal-qA9Jb92z.woff +0 -0
- package/web/dist/assets/antonio-latin-ext-700-normal-BdSkr58W.woff +0 -0
- package/web/dist/assets/antonio-latin-ext-700-normal-D-dvgqjr.woff2 +0 -0
- package/web/dist/assets/button-ClA70PLR.js +1 -0
- package/web/dist/assets/code-xml-CXkpHeU9.js +1 -0
- package/web/dist/assets/compare-view-DKl5l_Jw.js +2 -0
- package/web/dist/assets/copy-CCXzeW-s.js +1 -0
- package/web/dist/assets/cypher-view-Pd0WFH8F.js +13 -0
- package/web/dist/assets/design-view-CIHSmH4u.js +9 -0
- package/web/dist/assets/erd-view-CV7mFGAN.js +1 -0
- package/web/dist/assets/format-sql-BLj5_RDZ.js +5 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/graph-force-yBLyErpS.js +1 -0
- package/web/dist/assets/graph-link-OCo_s0zO.js +1 -0
- package/web/dist/assets/graph-view-sM1OlByf.js +3 -0
- package/web/dist/assets/index-CDcnUacl.js +10 -0
- package/web/dist/assets/index-OzJqnJWG.css +2 -0
- package/web/dist/assets/key-round-Cv_7QeED.js +1 -0
- package/web/dist/assets/loader-circle-BK_GMdvN.js +1 -0
- package/web/dist/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
- package/web/dist/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
- package/web/dist/assets/orbitron-latin-900-normal-BbJatbhp.woff +0 -0
- package/web/dist/assets/orbitron-latin-900-normal-DrIi7unX.woff2 +0 -0
- package/web/dist/assets/query-builder-view-ChNFb51g.js +4 -0
- package/web/dist/assets/rotate-ccw-CdCaf7Q6.js +1 -0
- package/web/dist/assets/route-DO6IZ7of.js +15 -0
- package/web/dist/assets/seed-view-DFNB8WHE.js +7 -0
- package/web/dist/assets/share-tech-mono-latin-400-normal-15MgdvEM.woff +0 -0
- package/web/dist/assets/share-tech-mono-latin-400-normal-CCqdbj5z.woff2 +0 -0
- package/web/dist/assets/shield-alert-CMS-3j4U.js +1 -0
- package/web/dist/assets/sliders-horizontal-Dg51yc-C.js +1 -0
- package/web/dist/assets/sql-tabs-B4jt6bQj.js +2 -0
- package/web/dist/assets/table-DfMz2f8M.js +1 -0
- package/web/dist/assets/triangle-alert-COGnHCD1.js +1 -0
- package/web/dist/assets/zone-D9dXk8tn.js +5 -0
- package/web/dist/index.html +26 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { render } from 'ink';
|
|
6
|
+
import meow from 'meow';
|
|
7
|
+
import App from './app.js';
|
|
8
|
+
import { createVaultClient, defaultRegistry, PromptRequiredError, } from './config/resolvers/index.js';
|
|
9
|
+
import { loadConfig } from './config/load.js';
|
|
10
|
+
import { dataDir } from './db/paths.js';
|
|
11
|
+
import { openStore } from './db/store.js';
|
|
12
|
+
import { testConnection } from './db/tester.js';
|
|
13
|
+
import { configgen } from './commands/configgen.js';
|
|
14
|
+
import { commandNames, isCommand, runCommand } from './commands/explore.js';
|
|
15
|
+
import { createTerminalPrompter } from './commands/terminal-prompt.js';
|
|
16
|
+
import { openSession } from './explorer/session.js';
|
|
17
|
+
import { createPromptQueue } from './prompts.js';
|
|
18
|
+
import { createStartup } from './startup.js';
|
|
19
|
+
import { createWebServer, DEFAULT_WEB_PORT, listen, openBrowser } from './web/server.js';
|
|
20
|
+
// meow reads package.json via importMeta, so the version and description have
|
|
21
|
+
// a single source of truth. It handles --help and --version (exit 0) itself.
|
|
22
|
+
const cli = meow(`
|
|
23
|
+
Usage
|
|
24
|
+
$ psql-explorer start the interactive TUI (also serves the web UI; --port picks its port)
|
|
25
|
+
$ psql-explorer envs
|
|
26
|
+
$ psql-explorer schemas --env <name>
|
|
27
|
+
$ psql-explorer tables --env <name> [--schema <schema>]
|
|
28
|
+
$ psql-explorer describe --env <name> <table>
|
|
29
|
+
$ psql-explorer rows --env <name> <table> [--limit 100] [--offset 0] [--order <col>] [--desc]
|
|
30
|
+
$ psql-explorer query --env <name> "<sql>" (or "-", or pipe the SQL in)
|
|
31
|
+
$ psql-explorer info --env <name>
|
|
32
|
+
$ psql-explorer history --env <name>
|
|
33
|
+
$ psql-explorer diff --env <name> --with <name> [--schema <schema>] [--with-schema <schema>]
|
|
34
|
+
$ psql-explorer --web [--port 7878] [--no-open] serve the web UI and open it
|
|
35
|
+
$ psql-explorer web same as --web
|
|
36
|
+
$ psql-explorer configgen:file
|
|
37
|
+
$ psql-explorer configgen:vault
|
|
38
|
+
$ psql-explorer secret:set <name> (asks for it, or pipe it in)
|
|
39
|
+
$ psql-explorer secret:delete <name>
|
|
40
|
+
|
|
41
|
+
Commands
|
|
42
|
+
envs List the environments: the shared ones and your local connections.
|
|
43
|
+
schemas List the schemas of the database.
|
|
44
|
+
tables List the tables and views of a schema, with row estimates and sizes.
|
|
45
|
+
describe Show a table's columns, indexes and constraints.
|
|
46
|
+
rows Print a page of a table's rows.
|
|
47
|
+
query Run one SQL statement and print what it returns. On a read-only
|
|
48
|
+
environment anything that would change data is refused by the server.
|
|
49
|
+
info Show the server version, database size and session settings.
|
|
50
|
+
history The statements run with query or the TUI, newest first.
|
|
51
|
+
diff Compare the structure of two environments' databases: tables,
|
|
52
|
+
columns, indexes, constraints, triggers, policies, views, functions,
|
|
53
|
+
sequences, types and extensions. All schemas, or the one --schema names.
|
|
54
|
+
web Serve the web UI and its API on http://127.0.0.1:<port> until stopped.
|
|
55
|
+
configgen:file Print a fully commented example environment kept in a directory.
|
|
56
|
+
configgen:vault Print the same for one kept in Vault.
|
|
57
|
+
secret:set Keep a secret in the local store, for config values written as
|
|
58
|
+
source: stored. secret:delete removes it.
|
|
59
|
+
|
|
60
|
+
Options
|
|
61
|
+
--config <base> Where the shared config lives (default: $PSQL_EXPLORER_ENVIRONMENTS): a
|
|
62
|
+
directory, or vault:<KV v2 path> such as vault:secret/data/psql-explorer.
|
|
63
|
+
It holds one <environment>/explorer.yaml per environment.
|
|
64
|
+
--env <name> The environment: a shared one, or a local connection by name
|
|
65
|
+
--schema <name> tables, describe, rows: the schema (default: the environment's);
|
|
66
|
+
diff: compare only this schema
|
|
67
|
+
--with <name> diff: the environment to compare --env with
|
|
68
|
+
--with-schema <name>
|
|
69
|
+
diff: the schema on the --with side, when it is named differently
|
|
70
|
+
--format <f> table (default), csv or json
|
|
71
|
+
--json Same as --format json
|
|
72
|
+
--limit <n> rows: rows per page (default 100); history: how many (default 50)
|
|
73
|
+
--offset <n> rows: rows to skip
|
|
74
|
+
--order <cols> rows: sort by these columns, comma separated
|
|
75
|
+
--desc rows: sort descending
|
|
76
|
+
--max-rows <n> query: read at most this many rows (default 1000)
|
|
77
|
+
--timeout <s> Cancel a statement after this many seconds (default 30, 0 for none)
|
|
78
|
+
--web Start the web UI instead of the TUI (same as the web command)
|
|
79
|
+
--no-open web: do not open the browser
|
|
80
|
+
--port <n> web: the port to listen on (default 7878; the next free one is used if taken)
|
|
81
|
+
--help Show this help
|
|
82
|
+
--version Show the version
|
|
83
|
+
--relnotes Show the release notes
|
|
84
|
+
|
|
85
|
+
Run without a command to start the interactive TUI.
|
|
86
|
+
`, {
|
|
87
|
+
importMeta: import.meta,
|
|
88
|
+
flags: {
|
|
89
|
+
config: { type: 'string' },
|
|
90
|
+
env: { type: 'string' },
|
|
91
|
+
with: { type: 'string' },
|
|
92
|
+
withSchema: { type: 'string' },
|
|
93
|
+
schema: { type: 'string' },
|
|
94
|
+
format: { type: 'string' },
|
|
95
|
+
json: { type: 'boolean' },
|
|
96
|
+
limit: { type: 'number' },
|
|
97
|
+
offset: { type: 'number' },
|
|
98
|
+
order: { type: 'string' },
|
|
99
|
+
desc: { type: 'boolean' },
|
|
100
|
+
maxRows: { type: 'number' },
|
|
101
|
+
timeout: { type: 'number' },
|
|
102
|
+
port: { type: 'number' },
|
|
103
|
+
web: { type: 'boolean' },
|
|
104
|
+
relnotes: { type: 'boolean' },
|
|
105
|
+
open: { type: 'boolean', default: true },
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
// RELEASES.md ships next to dist, like web/dist, and needs neither the database nor a config.
|
|
109
|
+
if (cli.flags.relnotes) {
|
|
110
|
+
try {
|
|
111
|
+
process.stdout.write(readFileSync(join(import.meta.dirname, '..', 'RELEASES.md'), 'utf8'));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
process.stderr.write('psql-explorer: the release notes are missing from this install\n');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Prints an example config and needs neither the local database nor a config.
|
|
120
|
+
const [command, ...args] = cli.input;
|
|
121
|
+
if (command === 'configgen:file' || command === 'configgen:vault') {
|
|
122
|
+
process.stdout.write(configgen(command === 'configgen:file' ? 'file' : 'vault'));
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
const vault = createVaultClient();
|
|
126
|
+
// Opened before the TTY check so a broken data dir is reported the same way everywhere.
|
|
127
|
+
let store;
|
|
128
|
+
try {
|
|
129
|
+
store = openStore(dataDir());
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
process.stderr.write(`psql-explorer: ${error instanceof Error ? error.message : String(error)}\n` +
|
|
133
|
+
'Check the directory permissions, or set PSQL_EXPLORER_CONFIG to another directory.\n');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
const readStdin = async () => {
|
|
137
|
+
const chunks = [];
|
|
138
|
+
for await (const chunk of process.stdin)
|
|
139
|
+
chunks.push(chunk);
|
|
140
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
141
|
+
};
|
|
142
|
+
// Secrets for `source: stored` values: they need the store but no config.
|
|
143
|
+
if (command === 'secret:set' || command === 'secret:delete') {
|
|
144
|
+
const [name] = args;
|
|
145
|
+
let code = 0;
|
|
146
|
+
if (!name) {
|
|
147
|
+
process.stderr.write(`psql-explorer: ${command} needs the secret's name\n`);
|
|
148
|
+
code = 2;
|
|
149
|
+
}
|
|
150
|
+
else if (command === 'secret:delete') {
|
|
151
|
+
if (!store.deleteSecret(name)) {
|
|
152
|
+
process.stderr.write(`psql-explorer: there is no stored secret named "${name}"\n`);
|
|
153
|
+
code = 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
try {
|
|
158
|
+
const value = process.stdin.isTTY
|
|
159
|
+
? await createTerminalPrompter().ask({ envName: 'secret', field: name, masked: true })
|
|
160
|
+
: // A trailing newline from echo or a file is not part of the secret.
|
|
161
|
+
(await readStdin()).replace(/\r?\n$/, '');
|
|
162
|
+
if (value) {
|
|
163
|
+
store.setSecret(name, value);
|
|
164
|
+
process.stderr.write(`Stored secret ${name}\n`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
process.stderr.write('psql-explorer: the secret is empty; nothing was stored\n');
|
|
168
|
+
code = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
process.stderr.write('psql-explorer: cancelled; nothing was stored\n');
|
|
173
|
+
code = 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
store.close();
|
|
177
|
+
process.exit(code);
|
|
178
|
+
}
|
|
179
|
+
if (command === 'web' || (command === undefined && cli.flags.web)) {
|
|
180
|
+
const { config, error: configError } = await loadConfig({ base: cli.flags.config, vault });
|
|
181
|
+
const prompts = createPromptQueue();
|
|
182
|
+
const services = {
|
|
183
|
+
store,
|
|
184
|
+
tester: testConnection,
|
|
185
|
+
config,
|
|
186
|
+
configError,
|
|
187
|
+
// Questions are answered in the browser: the page polls /api/prompt and posts the answer.
|
|
188
|
+
resolvers: defaultRegistry(undefined, prompts, store, vault),
|
|
189
|
+
prompts,
|
|
190
|
+
openSession,
|
|
191
|
+
};
|
|
192
|
+
const server = createWebServer({
|
|
193
|
+
services,
|
|
194
|
+
staticDir: join(import.meta.dirname, '..', 'web', 'dist'),
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
const requested = cli.flags.port ?? DEFAULT_WEB_PORT;
|
|
198
|
+
const address = await listen(server, requested);
|
|
199
|
+
const port = Number(new URL(address).port);
|
|
200
|
+
if (port !== requested) {
|
|
201
|
+
process.stderr.write(`psql-explorer: port ${requested} is in use, using port ${port} instead\n`);
|
|
202
|
+
}
|
|
203
|
+
process.stderr.write(`psql-explorer web UI on ${address} (Ctrl-C to stop)\n`);
|
|
204
|
+
if (cli.flags.open)
|
|
205
|
+
openBrowser(address);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
store.close();
|
|
209
|
+
process.stderr.write(`psql-explorer: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
process.once('SIGINT', () => {
|
|
213
|
+
server.close();
|
|
214
|
+
store.close();
|
|
215
|
+
process.exit(0);
|
|
216
|
+
});
|
|
217
|
+
await new Promise(() => { });
|
|
218
|
+
}
|
|
219
|
+
if (command !== undefined) {
|
|
220
|
+
let code = 2;
|
|
221
|
+
if (!isCommand(command)) {
|
|
222
|
+
process.stderr.write(`psql-explorer: unknown command "${command}"; use one of: ${commandNames.join(', ')}\n`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const { config, error: configError } = await loadConfig({ base: cli.flags.config, vault });
|
|
226
|
+
// A person at a terminal can be asked for what the config says to prompt for.
|
|
227
|
+
const prompter = process.stdin.isTTY
|
|
228
|
+
? createTerminalPrompter()
|
|
229
|
+
: { ask: ({ envName, field }) => Promise.reject(new PromptRequiredError(envName, field)) };
|
|
230
|
+
const services = {
|
|
231
|
+
store,
|
|
232
|
+
tester: testConnection,
|
|
233
|
+
config,
|
|
234
|
+
configError,
|
|
235
|
+
resolvers: defaultRegistry(undefined, prompter, store, vault),
|
|
236
|
+
openSession,
|
|
237
|
+
};
|
|
238
|
+
code = await runCommand({
|
|
239
|
+
services,
|
|
240
|
+
command,
|
|
241
|
+
args,
|
|
242
|
+
flags: cli.flags,
|
|
243
|
+
out: (text) => process.stdout.write(text),
|
|
244
|
+
err: (text) => process.stderr.write(text),
|
|
245
|
+
readStdin,
|
|
246
|
+
stdinIsTTY: process.stdin.isTTY === true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
store.close();
|
|
250
|
+
// Let piped output drain before exiting.
|
|
251
|
+
await new Promise((resolve) => process.stdout.write('', resolve));
|
|
252
|
+
process.exit(code);
|
|
253
|
+
}
|
|
254
|
+
// Ink needs raw-mode keyboard input; without a TTY it would crash, so fail clearly.
|
|
255
|
+
if (!process.stdin.isTTY) {
|
|
256
|
+
store.close();
|
|
257
|
+
process.stderr.write('psql-explorer: the TUI needs a terminal (stdin is not a TTY).\n' +
|
|
258
|
+
'Run it directly in a terminal, or use a command such as query (see --help).\n');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const prompts = createPromptQueue();
|
|
262
|
+
// The screen opens at once; reading the config (which may be in Vault) happens behind the splash
|
|
263
|
+
// screen, which shows each step. It fills in `config` and `configError`. What is wrong with the
|
|
264
|
+
// config is shown on the Environments screen (local connections still work, and so do the
|
|
265
|
+
// environments that are fine).
|
|
266
|
+
const services = {
|
|
267
|
+
store,
|
|
268
|
+
tester: testConnection,
|
|
269
|
+
resolvers: defaultRegistry(undefined, prompts, store, vault),
|
|
270
|
+
prompts,
|
|
271
|
+
openSession,
|
|
272
|
+
openUrl: openBrowser,
|
|
273
|
+
};
|
|
274
|
+
services.startup = createStartup({ services, base: cli.flags.config, vault });
|
|
275
|
+
// The web UI is served from the same process, so the Environments screen can say where it is.
|
|
276
|
+
// Questions it triggers are answered in the browser, so it gets a prompt queue of its own; the
|
|
277
|
+
// config is the one the splash screen loads. Failing to start it never stops the TUI.
|
|
278
|
+
const webPrompts = createPromptQueue();
|
|
279
|
+
const webServices = {
|
|
280
|
+
store,
|
|
281
|
+
tester: testConnection,
|
|
282
|
+
resolvers: defaultRegistry(undefined, webPrompts, store, vault),
|
|
283
|
+
prompts: webPrompts,
|
|
284
|
+
openSession,
|
|
285
|
+
get config() {
|
|
286
|
+
return services.config;
|
|
287
|
+
},
|
|
288
|
+
get configError() {
|
|
289
|
+
return services.configError;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
const webServer = createWebServer({
|
|
293
|
+
services: webServices,
|
|
294
|
+
staticDir: join(import.meta.dirname, '..', 'web', 'dist'),
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
services.webUrl = await listen(webServer, cli.flags.port ?? DEFAULT_WEB_PORT);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
webServer.close();
|
|
301
|
+
}
|
|
302
|
+
const app = render(_jsx(App, { version: cli.pkg.version ?? '0.0.0', fullScreen: true, services: services }),
|
|
303
|
+
// The app owns the whole terminal, and the shell's screen comes back when it exits.
|
|
304
|
+
{ alternateScreen: true });
|
|
305
|
+
await app.waitUntilExit();
|
|
306
|
+
webServer.close();
|
|
307
|
+
store.close();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// `psql-explorer configgen:file` and `psql-explorer configgen:vault` print a fully commented
|
|
2
|
+
// example environment file, ready to copy. Every key the loader accepts appears, optional ones
|
|
3
|
+
// commented out.
|
|
4
|
+
const sources = `A setting is plain text (\`host: db.prod.internal\`) or a source and a value:
|
|
5
|
+
source: plaintext | inline | env | file | stored | vault | prompt`;
|
|
6
|
+
const access = `# production | reference | demo | development | local. Required.
|
|
7
|
+
tier: production
|
|
8
|
+
|
|
9
|
+
# Shown with a shield and in red. Optional, default: true for the production tier.
|
|
10
|
+
protected: true
|
|
11
|
+
|
|
12
|
+
# Open every session read only (default_transaction_read_only), so nothing run here can change
|
|
13
|
+
# data. Optional, default: the value of protected.
|
|
14
|
+
readOnly: true
|
|
15
|
+
|
|
16
|
+
# Opening the environment first asks for two random words to be typed back, so it is never
|
|
17
|
+
# entered by accident. Optional, default: the value of protected.
|
|
18
|
+
confirm: true
|
|
19
|
+
|
|
20
|
+
# Offer the web UI's Seed page, which fills tables with generated rows. Optional, default: true
|
|
21
|
+
# for the development and local tiers, false otherwise.
|
|
22
|
+
# seeder: false
|
|
23
|
+
|
|
24
|
+
# Let the web UI's Design page run statements that create, alter and drop tables; without it the
|
|
25
|
+
# page only previews the SQL. Optional, default: true for the development and local tiers, false
|
|
26
|
+
# otherwise.
|
|
27
|
+
# designer: false
|
|
28
|
+
`;
|
|
29
|
+
const fileEnvironment = `# One environment: <environment>/explorer.yaml below the base path.
|
|
30
|
+
# Use with: psql-explorer --config <base path> (or export PSQL_EXPLORER_ENVIRONMENTS=<base path>)
|
|
31
|
+
# The environment is named after its folder: lower case letters, digits, - and _.
|
|
32
|
+
# ${sources.replaceAll('\n', '\n# ')}
|
|
33
|
+
# Unknown keys are an error.
|
|
34
|
+
|
|
35
|
+
${access}
|
|
36
|
+
# Where the database is. Required. Any of these can use a source instead of plain text.
|
|
37
|
+
host: db.prod.internal
|
|
38
|
+
port: 5432
|
|
39
|
+
database: app
|
|
40
|
+
|
|
41
|
+
# The schema browsing starts in. Optional, default: public
|
|
42
|
+
# schema: app
|
|
43
|
+
|
|
44
|
+
# Database login. Required.
|
|
45
|
+
username: readonly
|
|
46
|
+
|
|
47
|
+
# password (default) needs a password; none means the database takes no password.
|
|
48
|
+
auth: password
|
|
49
|
+
|
|
50
|
+
# Required when auth is password. Must be left out when auth is none.
|
|
51
|
+
password:
|
|
52
|
+
source: file
|
|
53
|
+
value: ./password.txt # relative to this file's folder; the whole content, trimmed
|
|
54
|
+
# Other ways to supply it:
|
|
55
|
+
# password: s3cret # plain text, in the file itself
|
|
56
|
+
# password: {source: env, value: DB_PASSWORD} # an environment variable
|
|
57
|
+
# password: {source: stored, value: prod-password} # a secret kept in the local store
|
|
58
|
+
# password: {source: prompt} # asked for when connecting
|
|
59
|
+
# password: {source: vault, value: "secret/data/db/prod#password"} # a key of a Vault KV v2 secret
|
|
60
|
+
|
|
61
|
+
# TLS. Optional, default: mode disable
|
|
62
|
+
tls:
|
|
63
|
+
# disable | require | verify-ca | verify-full
|
|
64
|
+
mode: verify-full
|
|
65
|
+
# CA certificate (PEM text). Required for verify-ca and verify-full.
|
|
66
|
+
ca:
|
|
67
|
+
source: file
|
|
68
|
+
value: ./certs/ca.pem
|
|
69
|
+
# Client certificate and its private key, for mutual TLS. Give both or neither.
|
|
70
|
+
# cert:
|
|
71
|
+
# source: file
|
|
72
|
+
# value: ./certs/client.pem
|
|
73
|
+
# key:
|
|
74
|
+
# source: file
|
|
75
|
+
# value: ./certs/client-key.pem
|
|
76
|
+
# Only when the key is encrypted.
|
|
77
|
+
# keyPassphrase:
|
|
78
|
+
# source: file
|
|
79
|
+
# value: ./certs/client-key.pass
|
|
80
|
+
`;
|
|
81
|
+
const vaultEnvironment = `# One environment, stored as the explorer.yaml key of a secret below the base path:
|
|
82
|
+
# vault kv put secret/psql-explorer/production explorer.yaml=@production.yaml
|
|
83
|
+
# Use with: psql-explorer --config vault:secret/data/psql-explorer
|
|
84
|
+
# The environment is named after the last part of the secret's path: lower case letters,
|
|
85
|
+
# digits, - and _. Who can read the secret decides who sees the environment.
|
|
86
|
+
# ${sources.replaceAll('\n', '\n# ')}
|
|
87
|
+
# \`file\` values are relative to the working directory here, not to a folder.
|
|
88
|
+
# Unknown keys are an error.
|
|
89
|
+
|
|
90
|
+
${access}
|
|
91
|
+
# Where the database is. Required. Any of these can use a source instead of plain text.
|
|
92
|
+
host:
|
|
93
|
+
source: vault
|
|
94
|
+
value: secret/data/db/prod#host # "<KV v2 data path>#<key>"
|
|
95
|
+
port: 5432
|
|
96
|
+
database: app
|
|
97
|
+
|
|
98
|
+
# The schema browsing starts in. Optional, default: public
|
|
99
|
+
# schema: app
|
|
100
|
+
|
|
101
|
+
username: readonly
|
|
102
|
+
auth: password # password (default) | none; none takes no password, so leave password out
|
|
103
|
+
password: # required when auth is password
|
|
104
|
+
source: vault
|
|
105
|
+
value: secret/data/db/prod#password
|
|
106
|
+
|
|
107
|
+
# TLS. Optional, default: mode disable
|
|
108
|
+
tls:
|
|
109
|
+
# disable | require | verify-ca | verify-full
|
|
110
|
+
mode: verify-full
|
|
111
|
+
# CA certificate (PEM text). Required for verify-ca and verify-full.
|
|
112
|
+
ca:
|
|
113
|
+
source: vault
|
|
114
|
+
value: secret/data/db/prod#ca
|
|
115
|
+
# Client certificate and key, for mutual TLS. Give both or neither.
|
|
116
|
+
# cert:
|
|
117
|
+
# source: vault
|
|
118
|
+
# value: secret/data/db/prod#cert
|
|
119
|
+
# key:
|
|
120
|
+
# source: vault
|
|
121
|
+
# value: secret/data/db/prod#key
|
|
122
|
+
# keyPassphrase: # only when the key is encrypted
|
|
123
|
+
# source: vault
|
|
124
|
+
# value: secret/data/db/prod#key-passphrase
|
|
125
|
+
`;
|
|
126
|
+
export function configgen(kind) {
|
|
127
|
+
return kind === 'file' ? fileEnvironment : vaultEnvironment;
|
|
128
|
+
}
|