@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +505 -2
  3. package/RELEASES.md +87 -0
  4. package/dist/app.js +39 -0
  5. package/dist/cli.js +307 -0
  6. package/dist/commands/configgen.js +128 -0
  7. package/dist/commands/explore.js +388 -0
  8. package/dist/commands/terminal-prompt.js +68 -0
  9. package/dist/config/load.js +324 -0
  10. package/dist/config/resolve-environment.js +112 -0
  11. package/dist/config/resolvers/env.js +12 -0
  12. package/dist/config/resolvers/file.js +24 -0
  13. package/dist/config/resolvers/groups.js +1 -0
  14. package/dist/config/resolvers/index.js +49 -0
  15. package/dist/config/resolvers/literal.js +7 -0
  16. package/dist/config/resolvers/prompt.js +159 -0
  17. package/dist/config/resolvers/stored.js +13 -0
  18. package/dist/config/resolvers/types.js +18 -0
  19. package/dist/config/resolvers/vault.js +283 -0
  20. package/dist/config/schema.js +24 -0
  21. package/dist/config/source.js +105 -0
  22. package/dist/db/connect.js +55 -0
  23. package/dist/db/identifiers.js +6 -0
  24. package/dist/db/paths.js +14 -0
  25. package/dist/db/store.js +323 -0
  26. package/dist/db/tester.js +190 -0
  27. package/dist/db/validate.js +99 -0
  28. package/dist/environments.js +140 -0
  29. package/dist/explorer/alter.js +493 -0
  30. package/dist/explorer/catalog.js +414 -0
  31. package/dist/explorer/connect-entry.js +42 -0
  32. package/dist/explorer/cypher-parse.js +478 -0
  33. package/dist/explorer/cypher.js +963 -0
  34. package/dist/explorer/ddl.js +502 -0
  35. package/dist/explorer/dependents.js +222 -0
  36. package/dist/explorer/design-load.js +254 -0
  37. package/dist/explorer/design.js +45 -0
  38. package/dist/explorer/edit.js +108 -0
  39. package/dist/explorer/explain.js +37 -0
  40. package/dist/explorer/filter.js +121 -0
  41. package/dist/explorer/format.js +134 -0
  42. package/dist/explorer/graph.js +679 -0
  43. package/dist/explorer/gucs.js +31 -0
  44. package/dist/explorer/profile.js +102 -0
  45. package/dist/explorer/references.js +95 -0
  46. package/dist/explorer/rows.js +64 -0
  47. package/dist/explorer/schema-diff.js +360 -0
  48. package/dist/explorer/search.js +309 -0
  49. package/dist/explorer/security.js +152 -0
  50. package/dist/explorer/seed.js +1376 -0
  51. package/dist/explorer/session.js +122 -0
  52. package/dist/explorer/stats.js +135 -0
  53. package/dist/explorer/timeout.js +13 -0
  54. package/dist/prompts.js +52 -0
  55. package/dist/screens/environments.js +229 -0
  56. package/dist/screens/explorer.js +145 -0
  57. package/dist/screens/header.js +8 -0
  58. package/dist/screens/index.js +13 -0
  59. package/dist/screens/new-connection.js +178 -0
  60. package/dist/screens/panes/common.js +35 -0
  61. package/dist/screens/panes/history.js +36 -0
  62. package/dist/screens/panes/info.js +61 -0
  63. package/dist/screens/panes/query.js +113 -0
  64. package/dist/screens/panes/table.js +178 -0
  65. package/dist/screens/panes/tables.js +142 -0
  66. package/dist/screens/prompt-modal.js +18 -0
  67. package/dist/screens/record-view.js +42 -0
  68. package/dist/screens/result-grid.js +126 -0
  69. package/dist/screens/scroll.js +12 -0
  70. package/dist/screens/splash.js +78 -0
  71. package/dist/screens/terminal.js +25 -0
  72. package/dist/screens/types.js +1 -0
  73. package/dist/services.js +1 -0
  74. package/dist/startup.js +84 -0
  75. package/dist/web/server.js +988 -0
  76. package/package.json +69 -5
  77. package/web/dist/assets/anton-latin-400-normal-AUNGEG_V.woff +0 -0
  78. package/web/dist/assets/anton-latin-400-normal-Byf51wtH.woff2 +0 -0
  79. package/web/dist/assets/anton-latin-ext-400-normal-BMODBQc6.woff +0 -0
  80. package/web/dist/assets/anton-latin-ext-400-normal-SyiqE2Jt.woff2 +0 -0
  81. package/web/dist/assets/anton-vietnamese-400-normal-2FfR1wHA.woff +0 -0
  82. package/web/dist/assets/anton-vietnamese-400-normal-CkBxLiRJ.woff2 +0 -0
  83. package/web/dist/assets/antonio-latin-400-normal-B7iMKlzw.woff +0 -0
  84. package/web/dist/assets/antonio-latin-400-normal-CZjzwNAN.woff2 +0 -0
  85. package/web/dist/assets/antonio-latin-700-normal-BY3mCh47.woff +0 -0
  86. package/web/dist/assets/antonio-latin-700-normal-DSH-lEDG.woff2 +0 -0
  87. package/web/dist/assets/antonio-latin-ext-400-normal-nE82dizb.woff2 +0 -0
  88. package/web/dist/assets/antonio-latin-ext-400-normal-qA9Jb92z.woff +0 -0
  89. package/web/dist/assets/antonio-latin-ext-700-normal-BdSkr58W.woff +0 -0
  90. package/web/dist/assets/antonio-latin-ext-700-normal-D-dvgqjr.woff2 +0 -0
  91. package/web/dist/assets/button-ClA70PLR.js +1 -0
  92. package/web/dist/assets/code-xml-CXkpHeU9.js +1 -0
  93. package/web/dist/assets/compare-view-DKl5l_Jw.js +2 -0
  94. package/web/dist/assets/copy-CCXzeW-s.js +1 -0
  95. package/web/dist/assets/cypher-view-Pd0WFH8F.js +13 -0
  96. package/web/dist/assets/design-view-CIHSmH4u.js +9 -0
  97. package/web/dist/assets/erd-view-CV7mFGAN.js +1 -0
  98. package/web/dist/assets/format-sql-BLj5_RDZ.js +5 -0
  99. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  100. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  101. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  102. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  103. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  104. package/web/dist/assets/graph-force-yBLyErpS.js +1 -0
  105. package/web/dist/assets/graph-link-OCo_s0zO.js +1 -0
  106. package/web/dist/assets/graph-view-sM1OlByf.js +3 -0
  107. package/web/dist/assets/index-CDcnUacl.js +10 -0
  108. package/web/dist/assets/index-OzJqnJWG.css +2 -0
  109. package/web/dist/assets/key-round-Cv_7QeED.js +1 -0
  110. package/web/dist/assets/loader-circle-BK_GMdvN.js +1 -0
  111. package/web/dist/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
  112. package/web/dist/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
  113. package/web/dist/assets/orbitron-latin-900-normal-BbJatbhp.woff +0 -0
  114. package/web/dist/assets/orbitron-latin-900-normal-DrIi7unX.woff2 +0 -0
  115. package/web/dist/assets/query-builder-view-ChNFb51g.js +4 -0
  116. package/web/dist/assets/rotate-ccw-CdCaf7Q6.js +1 -0
  117. package/web/dist/assets/route-DO6IZ7of.js +15 -0
  118. package/web/dist/assets/seed-view-DFNB8WHE.js +7 -0
  119. package/web/dist/assets/share-tech-mono-latin-400-normal-15MgdvEM.woff +0 -0
  120. package/web/dist/assets/share-tech-mono-latin-400-normal-CCqdbj5z.woff2 +0 -0
  121. package/web/dist/assets/shield-alert-CMS-3j4U.js +1 -0
  122. package/web/dist/assets/sliders-horizontal-Dg51yc-C.js +1 -0
  123. package/web/dist/assets/sql-tabs-B4jt6bQj.js +2 -0
  124. package/web/dist/assets/table-DfMz2f8M.js +1 -0
  125. package/web/dist/assets/triangle-alert-COGnHCD1.js +1 -0
  126. package/web/dist/assets/zone-D9dXk8tn.js +5 -0
  127. package/web/dist/index.html +26 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kazuro Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,506 @@
1
- # Temporary Holding Version
1
+ # psql-explorer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Explore your Postgres databases from the terminal: browse schemas and tables, page through rows,
4
+ look at columns, indexes and constraints, and run SQL, in an interactive TUI or from the command
5
+ line. It is in the spirit of pgweb, without a browser.
6
+
7
+ A team shares one set of environments without a database of its own: the config is a folder of
8
+ small YAML files, kept in a directory or in HashiCorp Vault, and every password is looked up when
9
+ it is needed (from Vault, an environment variable, a file, a prompt, ...). Production environments
10
+ are opened **read only** by default, so nothing typed there can change data.
11
+
12
+ > **Disclaimer:** psql-explorer is under active development and has not reached a stable release.
13
+ > It connects to real databases, including production ones, and runs the SQL you give it. Check
14
+ > which environment you are in, keep production read only, and expect breaking changes between
15
+ > versions. It is provided as is, without warranty (see [LICENSE](LICENSE)).
16
+
17
+ ## Requirements
18
+
19
+ - Node.js 22.13 or newer
20
+ - PostgreSQL databases
21
+ - An interactive terminal for the TUI. The commands (`tables`, `query`, ...) need none.
22
+
23
+ ## Install and run
24
+
25
+ ```sh
26
+ npx @kazurolabs/psql-explorer # once, without installing
27
+ npm i -g @kazurolabs/psql-explorer # or install the psql-explorer command
28
+ ```
29
+
30
+ ### Quick start
31
+
32
+ ```sh
33
+ export PSQL_EXPLORER_ENVIRONMENTS=vault:secret/data/psql-explorer # or a directory
34
+ export VAULT_ADDR=https://vault.example.com:8200
35
+ vault login # or VAULT_TOKEN, see below
36
+ psql-explorer
37
+ ```
38
+
39
+ Pick an environment and press `Enter`. Without a shared config psql-explorer still starts, and `n`
40
+ saves your own connections.
41
+
42
+ ## Web UI
43
+
44
+ ```sh
45
+ psql-explorer --web [--port 7878] [--no-open] # serves the UI and API on http://127.0.0.1:7878 and opens it
46
+ ```
47
+
48
+ The UI lives in `web/` (Vite, React, Tailwind, shadcn/ui) and is powered by the same config,
49
+ Vault lookups and read-only rules as the TUI. A value the config says to `prompt` for (a password, say) is asked for in a dialog in the page, and remembered until the server stops. Picking an environment opens its database: the schemas on the left open to
50
+ their tables, views, functions, sequences and types, and the one you pick is shown on the right
51
+ (columns, indexes, constraints, definition and rows for a table; the source of a function; and so on).
52
+ Every view has an address of its own, e.g. `#/staging/table/app/users`. The server listens on loopback only. From a checkout, `npm run dev:web` starts the API and Vite with hot reload together (run
53
+ `npm run install:web` once first), and `npm run build` compiles the CLI and builds `web/dist`.
54
+
55
+ ### Settings on the SQL page
56
+
57
+ The **Settings** button on the SQL page sets Postgres settings (GUCs) such as `statement_timeout`,
58
+ `work_mem`, `search_path` or a custom `app.tenant_id` on the session before each Run, Explain and
59
+ export. They are kept per environment in the browser. In a read only environment the read only
60
+ settings themselves cannot be changed.
61
+
62
+ ### Seed page
63
+
64
+ Beside **SQL**, a **Seed** tab fills a table with generated rows, for building up a development
65
+ database (`#/dev/seed/app/users`). Pick a table, then a generator for each column: Faker's people,
66
+ places, companies, internet, text, numbers and dates, plus a fixed value, one of a list, a counter, a
67
+ Faker template such as `{{person.firstName}}-{{number.int(99)}}`, or an existing row of the table
68
+ a foreign key points at. Each column starts on a suggestion from its name, type and constraints
69
+ (serials, identities and `now()` defaults keep their default; enums and `CHECK (x IN (...))` lists
70
+ become "one of a list", where each allowed value can be ticked or not and given a weight, so
71
+ `shipped` can come up six times as often as `paid`; a typed list takes weights one a line), and has
72
+ its own options, a share of NULLs and a "unique" switch. Choose
73
+ how many rows (up to 100,000), a locale and optionally a seed (the same seed makes the same rows),
74
+ preview the first ten, then insert. All rows go in one transaction, so a run adds every row or
75
+ none; "Skip rows that clash" adds `ON CONFLICT DO NOTHING` instead. A unique column tries 100 times
76
+ for a value it has not made yet and then fails the run; "Skip rows that run out of unique values"
77
+ leaves that row out instead. Either skip can add a few fewer rows than asked for, and the result
78
+ says how many were skipped. Your picks for a table are kept in the browser.
79
+
80
+ Columns can depend on each other, so rows hang together. **Add condition** under a column's
81
+ generator adds "when ‹another column› is / is not / is one of / > / < / is NULL ‹value›, use ‹this
82
+ generator›" cases: for a book, `num_pages` can be 10 to 1000 when `is_digital` is true and 100 to
83
+ 2000 otherwise, and `file_mb` NULL unless it is digital. The first case that matches is used, and
84
+ the column's own generator otherwise. A template can also use another column of the same row,
85
+ e.g. `{{row.first_name}}.{{row.last_name}}@{{internet.domainName}}` for an email that matches the
86
+ name. Columns are made in the order this needs, so conditions may point either way; a loop, or a
87
+ condition on a column left on its default (whose value is not known), is refused.
88
+
89
+ Rows can take values from other tables through **Linked tables**. Each foreign key of the table is
90
+ a link already (named after its constraint), and **Link a table** adds any other table, say a
91
+ lookup table. A link gives each new row one row of its table, and every column that reads the link
92
+ reads that same row, so all the columns of a composite foreign key always point at a row that
93
+ exists. Columns read a link with "Existing row it references" (a foreign key column),
94
+ **Column of a linked table** (any link and column), or `{{link.name.column}}` in a template. A
95
+ link either picks **a random row for each new row** (optionally each row once, for a one-to-one
96
+ key) or makes **new rows for each of its rows**: set the orders link of `order_lines` that way with
97
+ 3 to 5 rows for each, and every order gets 3 to 5 lines; the number of rows to insert then follows
98
+ from the table. Several such links make rows for every combination of their rows. **Counting up for
99
+ each linked row** starts again for each of them, for line numbers 1, 2, 3 of each order.
100
+
101
+ Array columns (`text[]`, `int[]`, an array of an enum) get a list per row: **Some of a list** picks
102
+ between a min and max of distinct values from the ones you give (an enum array starts with its
103
+ labels), and any other generator is repeated for each item, with a min and max number of items
104
+ per array (1 to 3 unless you say). A fixed value written as an array literal, like `{a,b}`, is used
105
+ as it is.
106
+
107
+ The tab is only offered where the environment says `seeder: true`, which is the default for the
108
+ `development` and `local` tiers (and for local connections). A read only environment can only
109
+ preview, unless it is protected and switched to write mode.
110
+
111
+ ### Graph page
112
+
113
+ A **Graph** tab beside **Compare** draws rows and the rows linked to them through foreign keys, so
114
+ "this order, its customer, its lines and their products" can be seen at once
115
+ (`#/dev/graph/app/orders?filters=...&depth=1`). It only reads, and works on every environment. Pick a
116
+ table, narrow it with the same filters as the Rows tab, choose a depth (0 to 3) and a direction
117
+ (rows it points at, rows pointing at it, or both), and press **Draw**. Each row is a dot coloured by its
118
+ table that pushes the others apart while its links pull it towards the rows it is linked to (a
119
+ force-directed layout). The dot is named by a label from the row's data, and under it, fainter, is the
120
+ name of its table. An arrow runs from the row holding a key to the row it points at (hover for the
121
+ key's name). Click a dot to follow its links, shift-click to see its row, and click a "+37 more in
122
+ order_lines" node to load the next rows of a key. Drag a dot to move it, drag the background to move
123
+ around, scroll to zoom, and **Re-layout** lays it out afresh and fits it to the screen. The legend hides
124
+ and shows tables and keys. (The earlier layout, in columns of cards by distance, is still in the code
125
+ as `Canvas` in `web/src/components/graph-view.tsx`, but the page no longer uses it.)
126
+
127
+ A **schema map** above the graph is a small entity relationship diagram of the table's schema: a box
128
+ for each table (with the number of its rows drawn) and an arrow for each foreign key. Click a table to
129
+ leave it out of the graph, which then goes through it as it does for a join table, and click an arrow to
130
+ stop following that key; a bar offers **Draw again** (or **Undo**) to apply the changes. Clusters of
131
+ rows that are not linked to each other push each other further apart than rows of one cluster do.
132
+
133
+ **Labels and tables** sets how the drawing is named, for each kind of row in it. The first time a table
134
+ shows up it is asked about: a row is named by its primary key (the values joined for a composite key)
135
+ until other fields are picked, and several picked fields are joined too. A table can also be
136
+ **removed from the graph**, which is for join tables of many to many relationships: its rows are not
137
+ drawn, and the rows at either end are linked directly by a dashed line (named `via` the table) as if it
138
+ were not there. This also works for a chain: with `orders` and `order_lines` both removed, a customer is
139
+ linked to the products it ordered (`via orders › order_lines`). The foreign keys are followed in either
140
+ direction through up to three removed tables, with one join that gives the distinct pairs of rows, and
141
+ the shortest way to each table is used. A table that a key leads to directly is not also reached the long
142
+ way. At most 20 rows are linked to a row this way, and the graph says when that cut some off. The tables that look like join tables (two or more keys leaving their rows) are marked.
143
+ Removing or restoring a table draws the graph again. These choices, and the keys not followed, are remembered in the browser for each
144
+ environment.
145
+
146
+ The Rows tab has a **Graph** link that starts from its filters, and the row drawer a graph button that
147
+ starts from that row.
148
+ In the **SQL** tab, a result whose columns include a table's primary key gets a checkbox on each row;
149
+ tick some, pick which table they are rows of (the ones the statement names come first) and follow
150
+ **Graph N rows** to draw exactly those rows (`?keys=...` in the address). The address carries the table, filters, depth, direction and the cards that were
151
+ opened, so a copied link draws the same graph. **Export** saves the drawing as SVG or PNG, or the nodes
152
+ (with their rows) and edges as JSON.
153
+
154
+ ### Cypher page
155
+
156
+ A **Cypher** tab queries the database in a small, read only subset of Cypher, the language of Neo4j. A
157
+ label is a table and a relationship is a foreign key, so you ask for the shape of what you want rather
158
+ than writing the joins. To find products that customers buy together:
159
+
160
+ ```cypher
161
+ MATCH (a:Product)<-[]-(c:Customer)-[]->(b:Product)
162
+ WHERE a.id < b.id
163
+ RETURN a, b, count(DISTINCT c) AS customers
164
+ ORDER BY customers DESC
165
+ LIMIT 25
166
+ ```
167
+
168
+ Rows that are returned (`a`, `b`) are drawn as a graph, linked as the pattern links them, and a number the
169
+ query returns for each link (`customers`) labels it and sets its thickness. There is also a table of the
170
+ result. **Show SQL** shows the statement the query becomes, with the values written in, and how each
171
+ relationship was joined. The query is also in the address (`#/dev/cypher?q=...&run=1`).
172
+
173
+ - **Labels** name a table: `Customer`, `customers` and `OrderLine` (for `order_lines`) all work, and a
174
+ name in backticks may carry a schema, as in `` `shop.customers` ``. The environment's schema is tried first;
175
+ a name found in two other schemas must be given with one.
176
+ - **Relationships** follow a foreign key: `-[]->` from the table holding it, `<-[]-` to it, `-[]-` either
177
+ way. Tables with no key between them are joined through the shortest chain of keys through other tables
178
+ (up to three), as customers to products are through orders and order lines; the direction of the arrow
179
+ is not used for such a chain. When two keys, or two chains, are equally good, the error lists them and
180
+ a hint in the brackets picks one: the name of a key or its column (`-[:billing_address_id]-`), or a
181
+ table on the way (`-[:orders]-`). The nodes along a chain can also be written out.
182
+ A relationship cannot be named, and has no properties; paths of any length (`*`) are not supported.
183
+ - **Properties** in a node (`(c:Customer {country: 'NZ'})`) and **WHERE** narrow the rows: `=`, `<>`, `<`,
184
+ `<=`, `>`, `>=`, `IN [...]`, `IS [NOT] NULL`, `STARTS WITH`, `ENDS WITH`, `CONTAINS`, `=~` (a regular
185
+ expression for the whole value), `AND`, `OR`, `NOT`, and `toLower`, `toUpper`, `coalesce`, `abs`. Two
186
+ rows are compared with `=` and `<>` by their primary keys, which is how to leave out a row paired with
187
+ itself (`a <> b`), or to count each pair once (`a.id < b.id`).
188
+ - **RETURN** takes rows, columns (`c.name`, optionally `AS`) and `count`, `sum`, `avg`, `min`, `max` and
189
+ `collect`, which group by whatever else is returned; with `DISTINCT`, `ORDER BY`, `SKIP` and `LIMIT`.
190
+ `RETURN *` returns every named node.
191
+ - Two relationships that follow the same keys use different rows, as in Cypher (so a row is never its own
192
+ neighbour); the same row may still be matched by two nodes, which `<>` rules out.
193
+ - Not supported: anything that writes, `OPTIONAL MATCH`, `WITH`, `UNWIND`, `UNION`, parameters and
194
+ arithmetic.
195
+
196
+ Before a query runs, the SQL it becomes is **planned** (`EXPLAIN`, which runs nothing) and assessed. A
197
+ query that looks heavy shows what makes it so: a pattern in parts that nothing joins (a cross join), many
198
+ tables, grouping or sorting large tables that nothing narrows, or a plan that Postgres estimates at a high
199
+ cost or with a join producing millions of rows. A medium warning is shown with the result. A high one is
200
+ not run until **Run anyway** is pressed. A query that runs does so in a read only transaction that is
201
+ stopped after 15 seconds, and reads at most 200 rows (1000 with a `LIMIT`).
202
+
203
+ ### Build page
204
+
205
+ A **Build** tab makes a `SELECT` from a form, for when you would rather click than type
206
+ (`#/dev/build`). Pick a table to start from, then add what you need and the SQL is written beside it as
207
+ you go:
208
+
209
+ - **Join**: the tables related by a foreign key are offered first, with the columns that match already
210
+ filled in. Any other table can be joined too, on any two columns you pick: a foreign key is not
211
+ needed. A join can be inner, left, right, full or cross, take several tests (`=`, `<>`, `<`, ...), and
212
+ compare as text when the types differ. Each test says whether it follows a foreign key, and columns of
213
+ different types are flagged before Postgres refuses them.
214
+ - **Columns**: any column of any table, a count of rows, or a count, count distinct, sum, average, min
215
+ or max; a date can be cut into days, months, ... in a time zone. Columns that are not totals are
216
+ grouped by.
217
+ - **Filter**: tests that fit the column's type (contains, starts with, one of a list, between, within
218
+ the last 7 days, is empty, ...), against a value or another column, in groups of "all of" or "any
219
+ of" inside each other. A filter that is not filled in yet is left out. **Having** filters the totals.
220
+ - **Sort and limit**, and unique rows only.
221
+
222
+ **Run** shows the result with pages and export, **Edit as SQL** opens the statement in the SQL page, and
223
+ the address carries the whole form, so a link opens the same query. The form is also kept in the
224
+ browser per environment. The page only ever writes a `SELECT` (values are quoted, names are quoted when
225
+ they need it, and a statement is checked to read before it runs), so it works the same in a read only
226
+ environment. Joins, filters, grouping, sorting and a limit are covered; subqueries, `WITH` and window
227
+ functions are for the SQL page.
228
+
229
+ ### Design page
230
+
231
+ A **Design** tab makes tables from a form and changes the ones that exist, always showing the SQL it
232
+ will run (`#/dev/design/new`, and `#/dev/design/app/orders` to change `app.orders`; the table view
233
+ has an **Edit design** button). A form holds the table (schema, name, comment, unlogged, partitioning
234
+ by range, list or hash), a grid of columns (add, remove, reorder; built-in types with their sizes,
235
+ the database's enums, domains and composite types, arrays, `NOT NULL`, a default, identity, a stored
236
+ generated expression and a comment), constraints (primary key, unique, check, foreign key with its
237
+ actions, `MATCH FULL` and `DEFERRABLE`) and indexes (columns or expressions, unique, btree, hash, gin,
238
+ gist or brin, a partial `WHERE`, `INCLUDE`). Problems show as you type: a duplicate name, a foreign key
239
+ whose column types differ, a primary key on a nullable column (it becomes `NOT NULL`). The SQL is
240
+ formatted beside the form, with **Copy** and **Open in SQL**. **Create** runs it in one transaction and
241
+ names the statement that failed; afterwards **Seed it** opens the Seed page on the new table where
242
+ the environment allows seeding. A new form is kept in the browser until the table is created or cleared.
243
+
244
+ Editing loads the table into the same form and works out the `ALTER` statements from what you change:
245
+ adding, dropping and renaming columns, constraints and indexes (rows are followed by their own id, so
246
+ a rename is a `RENAME` and not a drop and an add), a changed type with a `USING` expression you can
247
+ write, defaults, `NOT NULL`, identity, comments, and renaming the table or moving it to another schema.
248
+ Partitioning, exclusion constraints and indexes with a custom operator class or ordering cannot be
249
+ changed here, and the page says which parts it leaves alone. Next to a statement are the warnings
250
+ that apply, with the table's row estimate: a type change that rewrites the table, `NOT NULL` or a new
251
+ constraint that reads every row. A foreign key or check can be added `NOT VALID` and then validated in a
252
+ second statement, and an index can be built `CONCURRENTLY`, which cannot run in a transaction and so
253
+ runs afterwards. Everything else runs in one transaction that starts with `SET LOCAL lock_timeout`
254
+ (5 seconds, editable), so a busy table fails fast instead of queueing behind other locks.
255
+
256
+ Anything that drops something (a table, column, constraint or index) lists what depends on it first
257
+ (views, foreign keys from other tables, functions), adds `CASCADE` only when ticked, and asks for two
258
+ random words before it runs, as write mode does. **Drop table** is a separate button.
259
+
260
+ The page needs `designer: true`, which defaults to on for the `development` and `local` tiers; where it
261
+ is off, or the environment is read only (unless it is protected and switched to write mode), the SQL
262
+ can only be previewed. The search box (`Cmd/Ctrl-K`) has **New table** and `edit design of orders`.
263
+
264
+ ### Compare page
265
+
266
+ A **Compare** tab shows how this environment's database is built differently from another's, for
267
+ checking that staging matches production before a release (`#/staging/compare/production`). It
268
+ compares every schema, or one, which may have another name on the other side (`shop` against
269
+ `shop_next`). Tables with their columns, indexes, constraints, triggers and row level security
270
+ policies, views, materialized views, functions, sequences, types and extensions are each listed as
271
+ differing, or only on one side; what differs is shown side by side, and view and function
272
+ definitions line by line. Owners, grants, comments, column order and the objects an extension
273
+ creates are not compared. Filter by name or by kind of difference, open either side's object, or
274
+ copy the whole comparison as text. Only the catalogs are read, so it works on read only
275
+ environments, and a password the other environment asks for is asked on this page.
276
+
277
+ ## Commands
278
+
279
+ ```sh
280
+ psql-explorer # the interactive TUI
281
+ psql-explorer envs # the environments you can use
282
+ psql-explorer schemas --env staging
283
+ psql-explorer tables --env staging [--schema app]
284
+ psql-explorer describe --env staging users # or app.users
285
+ psql-explorer rows --env staging users --limit 20 --order created_at --desc
286
+ psql-explorer query --env staging "select count(*) from orders"
287
+ echo "select now()" | psql-explorer query --env staging
288
+ psql-explorer info --env staging
289
+ psql-explorer history --env staging
290
+ psql-explorer diff --env staging --with production [--schema app] [--with-schema app_v2]
291
+ psql-explorer configgen:file # a commented example environment file
292
+ psql-explorer configgen:vault
293
+ psql-explorer secret:set prod-password # for `source: stored`; asks, or reads stdin
294
+ psql-explorer secret:delete prod-password
295
+ ```
296
+
297
+ Every command prints a table like psql's; `--format csv` or `--format json` (or `--json`) give
298
+ output for other tools. In JSON, numbers that fit a JavaScript number, booleans and `json`/`jsonb`
299
+ values are themselves; `bigint`, `numeric` and everything else are the text Postgres sent, so no
300
+ digit is lost.
301
+
302
+ | Option | Used for |
303
+ | ----------------- | --------------------------------------------------------------------------- |
304
+ | `--config <base>` | the shared config's base path; default `$PSQL_EXPLORER_ENVIRONMENTS` |
305
+ | `--env <name>` | the environment: a shared one, else a local connection of that name |
306
+ | `--schema <name>` | `tables`, `describe`, `rows`: the schema; default the environment's |
307
+ | `--format <f>` | `table` (default), `csv` or `json` |
308
+ | `--limit <n>` | `rows`: rows per page (default 100); `history`: how many (default 50) |
309
+ | `--offset <n>` | `rows`: rows to skip |
310
+ | `--order <cols>` | `rows`: sort by these columns, comma separated; `--desc` for descending |
311
+ | `--max-rows <n>` | `query`: read at most this many rows (default 1000), the rest are not read |
312
+ | `--timeout <s>` | the server cancels a statement after this many seconds (default 30, 0 none) |
313
+
314
+ Exit codes: `0` done, `1` failed (the reason is on stderr), `2` wrong command line. A value the
315
+ config says to `prompt` for is asked for on the terminal (a secret without echo); with no terminal
316
+ the command fails and names the value.
317
+
318
+ `query` runs one statement at a time. Each statement is kept in the history of that environment
319
+ (in your local data, see below), with how long it took and how many rows it returned.
320
+
321
+ ## The TUI
322
+
323
+ The **Environments** screen lists the shared environments and your local connections, with their
324
+ tier, address, TLS mode and whether they are read only. `Enter` explores one, `t` tests the
325
+ connection, `n` adds a local connection, `e` and `d` edit and delete one.
326
+
327
+ Exploring an environment has four tabs (`1`–`4`, or `Tab`):
328
+
329
+ 1. **Tables** — the tables, views and materialized views of a schema, with row estimates, sizes
330
+ and comments. `/` filters, `s` picks another schema (remembered per environment), `Enter` opens
331
+ a table on its **Rows** (a page at a time, sorted by the primary key), its **Structure**
332
+ (columns, types, defaults) and its **Indexes & constraints**; `Tab` switches between them.
333
+ In the rows, `n`/`p` page, `o` sorts by the selected column (again for descending, a third time
334
+ for the default), `c` counts the rows exactly, and `Enter` shows a row field by field.
335
+ 2. **Query** — type a statement and press `Enter`; `↑`/`↓` recall earlier ones. The result is a
336
+ grid like the rows. `Esc` leaves the editor, `e` returns to it, `r` runs it again.
337
+ 3. **History** — what was run against this environment, newest first. `Enter` puts a statement
338
+ back in the Query tab.
339
+ 4. **Info** — the server version, database size and session settings, and the other connections
340
+ to the database.
341
+
342
+ In every grid the arrow keys (or `h` `j` `k` `l`) move a cell at a time, `PgUp`/`PgDn` a page,
343
+ `g`/`G` to the first and last row, `0`/`$` to the first and last column. `Esc` goes back, `q`
344
+ quits (when not typing), and so does `Ctrl+C`.
345
+
346
+ ## Read only, and other safety
347
+
348
+ - An environment with `readOnly: true` opens every session with
349
+ `default_transaction_read_only = on`, so the server refuses `INSERT`, `UPDATE`, `DELETE`, DDL
350
+ and the like. `readOnly` defaults to `protected`, which defaults to true for the `production`
351
+ tier. The header shows **READ ONLY**. This guards against mistakes; it is not a permission
352
+ system, so give people a database role that can only read where it matters (Vault's database
353
+ secrets engine is a good way to hand those out, see below).
354
+ - An environment with `confirm: true` makes you type back two random words (for example
355
+ `maple ticket`) before it is opened, in the TUI, the web UI and on the terminal, so a protected
356
+ environment is never entered by accident, even when its password is stored in the config.
357
+ `confirm` defaults to `protected`; set `confirm: false` to turn it off. It is asked once per run
358
+ of the program, nothing is looked up before it is answered, and it fails (rather than skips)
359
+ when nobody can be asked, e.g. in a script with no terminal. The web UI also shows a banner
360
+ above the SQL box on a protected environment.
361
+ - In the web UI, the SQL box of a protected, read only environment has a switch at its top right.
362
+ Turning it on asks for two random words again (always, whatever `confirm` says), then every
363
+ connection of that environment is opened read write until you switch it off or stop the server.
364
+ The banner turns red and pulses while it is on. Turning it off needs no words.
365
+ - Every statement has a time limit (`statement_timeout`, 30 s unless `--timeout` says otherwise),
366
+ and a query reads at most `--max-rows` rows (1000 in the TUI), so a `SELECT *` on a big table
367
+ does not fill your memory.
368
+ - Values are shown as the text Postgres sends: no time zone conversion, no rounding.
369
+
370
+ ## Configuration
371
+
372
+ ### Environment variables
373
+
374
+ | Variable | Used for |
375
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------ |
376
+ | `PSQL_EXPLORER_ENVIRONMENTS` | the shared config's base path, a directory or `vault:<path>`; `--config` wins |
377
+ | `PSQL_EXPLORER_CONFIG` | the directory holding `explorer.db` (local connections, stored secrets, settings, query history) |
378
+ | `XDG_CONFIG_HOME` | without `PSQL_EXPLORER_CONFIG`, `explorer.db` is in `$XDG_CONFIG_HOME/psql-explorer` |
379
+ | | (else `~/.config/psql-explorer`) |
380
+ | `VAULT_ADDR` | Vault's address (required for anything in Vault) |
381
+ | `VAULT_TOKEN` | the Vault token to use |
382
+ | `VAULT_ROLE_ID`, `VAULT_SECRET_ID` | log in to Vault through AppRole |
383
+ | `VAULT_JWT`, `VAULT_ROLE` | log in to Vault with a JWT / OIDC token |
384
+ | `VAULT_NAMESPACE` | the Vault Enterprise namespace |
385
+ | any name | read by an environment's `source: env` values, for example `STAGING_DB_PASSWORD` |
386
+
387
+ Empty values count as unset. `explorer.db` is created readable by its owner only.
388
+
389
+ ### Shared configuration
390
+
391
+ Environments shared by a team live under a **base path**, given with `--config <base>` or
392
+ `$PSQL_EXPLORER_ENVIRONMENTS`. It holds one folder per environment, each with an `explorer.yaml`:
393
+
394
+ ```
395
+ <base>/staging/explorer.yaml one environment; the folder name is its name
396
+ <base>/production/explorer.yaml
397
+ ```
398
+
399
+ The base path is a directory (a git checkout, a shared drive, ...) or Vault:
400
+
401
+ ```sh
402
+ psql-explorer --config ~/team/explorer-config
403
+ psql-explorer --config vault:secret/data/psql-explorer # a KV v2 "<mount>/data/<path>"
404
+ ```
405
+
406
+ Environments are found by listing the base path. A folder without an `explorer.yaml` is ignored.
407
+ An environment file that is wrong is reported (on the Environments screen, or on stderr) and left
408
+ out; the others still load.
409
+
410
+ #### Config in Vault
411
+
412
+ In Vault each environment is the `explorer.yaml` key of a secret below the base path:
413
+ `secret/data/psql-explorer/staging` is the `staging` environment. psql-explorer lists
414
+ `secret/metadata/psql-explorer` to find them, so **what a person sees follows their Vault policy**:
415
+ an environment they may not read is simply not in their list.
416
+
417
+ ```sh
418
+ vault kv put secret/psql-explorer/staging explorer.yaml=@staging.yaml
419
+ ```
420
+
421
+ ```hcl
422
+ path "secret/metadata/psql-explorer" { capabilities = ["list"] }
423
+ path "secret/data/psql-explorer/staging" { capabilities = ["read"] } # one per environment
424
+ # or: path "secret/data/psql-explorer/*" { capabilities = ["read"] }
425
+ ```
426
+
427
+ ### An environment file
428
+
429
+ `psql-explorer configgen:file` (or `configgen:vault`) prints a fully commented example. In short:
430
+
431
+ ```yaml
432
+ tier: production # production | reference | demo | development | local
433
+ protected: true # optional; default: tier is production
434
+ readOnly: true # optional; default: protected
435
+ confirm: true # optional; default: protected (type two random words to open it)
436
+ seeder: false # optional; default: tier is development or local (the web UI's Seed page)
437
+ designer: false # optional; default: tier is development or local (the web UI's Design page runs SQL)
438
+ host: db.prod.internal
439
+ port: 5432
440
+ database: app
441
+ schema: app # optional; where browsing starts, and first on the search_path; default public
442
+ username: readonly
443
+ password: # or `auth: none` for a server that takes no password
444
+ source: vault
445
+ value: secret/data/db/prod#password
446
+ tls:
447
+ mode: verify-full # disable | require | verify-ca | verify-full
448
+ ca: {source: file, value: ./ca.pem}
449
+ ```
450
+
451
+ Any of `host`, `port`, `database`, `schema`, `username`, `password` and the TLS `ca`, `cert`,
452
+ `key` and `keyPassphrase` can be written as plain text or as a `source` and a `value`:
453
+
454
+ | `source` | the value is |
455
+ | --------------------- | ---------------------------------------------------------------------------------- |
456
+ | `plaintext`, `inline` | the text in `value` |
457
+ | `env` | the environment variable named in `value` |
458
+ | `file` | the content of the file in `value`, relative to the environment's folder |
459
+ | `stored` | a secret of that name in your local store (`psql-explorer secret:set <name>`) |
460
+ | `prompt` | asked for when connecting (no `value`), and remembered until the program ends |
461
+ | `vault` | a key of a KV v2 secret: `value: <data path>#<key>`, e.g. `secret/data/db/prod#pw` |
462
+
463
+ ### Vault
464
+
465
+ Vault needs `VAULT_ADDR`, and a token from the first of: `VAULT_TOKEN`; an AppRole login with
466
+ `VAULT_ROLE_ID` and `VAULT_SECRET_ID`; a JWT login with `VAULT_JWT` and `VAULT_ROLE`; the
467
+ `~/.vault-token` the `vault` CLI writes on `vault login`. Only plain KV v2 secrets are read, not
468
+ dynamic ones such as the database or PKI engines. Secrets are read each time they are needed and
469
+ never kept, so a rotated password is picked up at once. A batch of queries that runs side by side
470
+ (the value search reads tables on up to 4 connections) reads them once and shares the result. An expired token is reported
471
+ as such, with a reminder to `vault login`.
472
+
473
+ ### Local connections
474
+
475
+ Press `n` on the Environments screen to save a connection of your own (host, port, database,
476
+ username, password, schema and SSL). They live in `explorer.db` with the password unencrypted,
477
+ in a file only you can read, are never read only, and can be used by name with `--env`.
478
+
479
+ ## Development
480
+
481
+ ```sh
482
+ npm install
483
+ npm run build # compile src/ to dist/ with tsc
484
+ node dist/cli.js # run the built CLI
485
+ npm test # run the tests (Vitest; builds dist/ first)
486
+ npm run lint # typecheck + ESLint + Prettier check
487
+ npm run format # apply Prettier formatting
488
+ ```
489
+
490
+ `npm run dev` starts psql-explorer against a demo: a `demo_shop` database (customers, products,
491
+ orders, a 200,000-row `page_views` table with no index on `customer_id` for trying Explain, a view,
492
+ a materialized view and an `audit` schema) on the `psql-explorer-pg` container
493
+ (port 55440, started if it is not running; set `DEMO_PG_URL` to use another server), and a shared
494
+ config in `.demo/config` with a read-write, a read-only and a protected environment. Its data
495
+ directory is `.demo/data`, so your real `~/.config/psql-explorer` is untouched.
496
+ `npm run dev:web` starts the web UI against the same demo. `npm run dev:reset` and
497
+ `npm run dev:web:reset` build the demo again. `npm run dev:plain` runs the TUI without it.
498
+
499
+ The tests that need a real Postgres run when `PSQL_EXPLORER_TEST_PG_URL` is set, and are skipped
500
+ otherwise:
501
+
502
+ ```sh
503
+ PSQL_EXPLORER_TEST_PG_URL=postgres://postgres:postgres@localhost:55440/app npm test
504
+ ```
505
+
506
+ Each run creates a schema of its own (`explorer_test_<random>`) and drops it afterwards.
package/RELEASES.md ADDED
@@ -0,0 +1,87 @@
1
+ # Release notes
2
+
3
+ ## 0.3.0 (2026-11-02)
4
+
5
+ ### 2026-11-01
6
+
7
+ - [cli] Add a --relnotes flag that prints the release notes
8
+ - Fix: Do not draw nodes for the null side of an outer join in the graph
9
+ - Fix: Leave out rows whose chosen label is null from the row graph
10
+
11
+ ## 0.2.0 (2026-11-01)
12
+
13
+ Everything built so far, from the commit summaries, oldest first. Print it with `psql-explorer --relnotes`.
14
+
15
+ ### 2026-10-28
16
+
17
+ - [cli] Initial psql-explorer CLI and TUI
18
+
19
+ ### 2026-10-29
20
+
21
+ - Database view with schema navigation and object details
22
+ - SQL editor with paged results, ERD view and themes
23
+ - Explain button describing a query plan in plain English
24
+ - Answer prompted config values in the browser
25
+ - Confirm protected environments with two random words
26
+ - Switch protected databases to write mode from the SQL box
27
+
28
+ ### 2026-10-30
29
+
30
+ - Filter, follow keys, inspect rows and profile columns
31
+ - Open a table as a pre-filled query in the SQL box
32
+ - Deep links carry the SQL statement, tab and page, and refuse unknown databases
33
+ - [tui] Serve the web UI from the TUI and show where it is running
34
+ - Fix: Keep the address bounded so a huge statement cannot stop a query from running
35
+ - Open several query tabs on the SQL page and rename them
36
+ - Copy and share buttons on the SQL page, and paste a share code to open it
37
+ - Numbered pages and a go-to-page box on results pages
38
+ - Export results, query history and saved queries, and cancel with timing
39
+ - Result charts with image export, and inline row editing in write mode
40
+ - Group by day, month and more of date columns, with a time zone setting
41
+ - Format SQL, run scripts statement by statement, and read special types
42
+ - Table stats, an explain plan tree and a dependency view
43
+ - A Seed page that fills tables with generated rows
44
+ - Fix: Only pop up questions for the environment the page is on
45
+ - Skip seeded rows that run out of unique values
46
+ - Link seeded rows to other tables, and make rows for each of their rows
47
+ - Tick and weight the allowed values of a seeded column
48
+ - Compare schemas between environments on a Compare page and with diff
49
+ - Give the dev demo environment a database with a different schema
50
+ - Docs: Plan a record graph and a table designer in TASKS2.md
51
+ - Add a designer setting for environments
52
+ - Add a graph API for rows linked by foreign keys
53
+ - Add a DDL builder for table designs
54
+ - Add a Graph page that draws rows linked by foreign keys
55
+ - Add Graph entry points, a shareable address and export
56
+
57
+ ### 2026-10-31
58
+
59
+ - Add a Design page that creates tables from a form
60
+ - Edit and drop tables from the Design page
61
+ - Docs: Document the Design page and add New table and Edit design to the search box
62
+ - Add a Cypher page that queries foreign keys as a graph, and rework the Graph page
63
+ - Add a Settings editor for GUCs on the SQL page
64
+
65
+ ### 2026-11-01
66
+
67
+ - Explain functions and column defaults when hovering SQL
68
+ - Autocomplete function names in the SQL editor
69
+ - Show parameter help for the function call being written in the SQL editor
70
+ - Update the sidebar when DDL changes the database objects
71
+ - Autocomplete column types in CREATE TABLE and split the bundle
72
+ - Autocomplete columns in the column list of CREATE INDEX
73
+ - Autocomplete tables after ON and REFERENCES and columns in more DDL
74
+ - Add an LCARS theme with console sounds, a star chart graph and block navs
75
+ - Show row level security policies on tables and the roles of the server
76
+ - Show security details for views and functions
77
+ - Add a dark variant of the standard theme
78
+ - Fix: Share the standard title styling with the dark variant
79
+ - Give each theme its own splash text
80
+ - Add a Build page that makes SELECT queries from a form
81
+ - Drop a file onto a field in the row editor
82
+ - Raise the dropped file limit to 20 MB
83
+ - Fix: Accept a dropped file in an edit request
84
+ - Add a download link for bytea fields in the row viewer
85
+ - Detect more file types and offer a gzip download for bytea
86
+ - Fix: [cli] List confirm in the configgen examples
87
+ - Add a warp field behind the environment list in the lcars theme
package/dist/app.js ADDED
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState, useSyncExternalStore } from 'react';
3
+ import { Box, useApp, useInput, useStdin } from 'ink';
4
+ import { screens as defaultScreens, } from './screens/index.js';
5
+ import PromptModal from './screens/prompt-modal.js';
6
+ import { useTerminalSize } from './screens/terminal.js';
7
+ const noSubscribe = () => () => undefined;
8
+ const noPrompt = () => undefined;
9
+ export default function App({ version, services, initialScreen = 'splash', initialParams, fullScreen = false, screens = defaultScreens, }) {
10
+ const [route, setRoute] = useState({
11
+ name: initialScreen,
12
+ params: initialParams,
13
+ key: 0,
14
+ });
15
+ const { exit } = useApp();
16
+ const { columns, rows } = useTerminalSize();
17
+ const { isRawModeSupported } = useStdin();
18
+ const navigate = useCallback((name, ...[params]) => {
19
+ setRoute((previous) => ({ name, params, key: previous.key + 1 }));
20
+ }, []);
21
+ const entry = screens[route.name];
22
+ const { prompts } = services;
23
+ const prompt = useSyncExternalStore(prompts ? prompts.subscribe : noSubscribe, prompts ? prompts.current : noPrompt);
24
+ const screenInput = isRawModeSupported && !prompt;
25
+ // Global quit key, off on screens that take text input; Ctrl+C is handled by Ink (exitOnCtrlC).
26
+ useInput((input) => {
27
+ if (input === 'q')
28
+ exit();
29
+ }, { isActive: screenInput && !entry.capturesInput });
30
+ // The registry ties each component to its own params type; the route carries the params that
31
+ // navigate() type-checked for this screen, so widening here is safe.
32
+ const Screen = entry.component;
33
+ const screen = (_jsx(Screen, { version: version, navigate: navigate, params: route.params, services: services, isInputActive: screenInput }, route.key));
34
+ const modal = prompt && (_jsx(PromptModal, { prompt: prompt, isInputActive: isRawModeSupported }, `${prompt.envName}.${prompt.field}`));
35
+ if (!fullScreen) {
36
+ return (_jsxs(_Fragment, { children: [screen, modal] }));
37
+ }
38
+ return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", justifyContent: prompt ? 'center' : 'flex-start', children: [_jsx(Box, { display: prompt ? 'none' : 'flex', flexDirection: "column", flexGrow: 1, children: screen }), modal] }));
39
+ }