neon 2.47.0 → 3.1.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 (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +395 -0
  5. package/dist/_shared/env-core/reuse-secrets.js +179 -0
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +575 -658
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +642 -681
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -341
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -64
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +133 -147
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +9 -7
@@ -1,64 +1,6 @@
1
- /**
2
- * Connection backslash commands.
3
- *
4
- * TypeScript port of the corresponding `exec_command_*` functions in
5
- * upstream PostgreSQL's `src/bin/psql/command.c`:
6
- *
7
- * - `\c` / `\connect` → exec_command_connect / do_connect
8
- * - `\conninfo` → exec_command_conninfo
9
- * - `\encoding` → exec_command_encoding
10
- * - `\password` → exec_command_password
11
- *
12
- * What this module owns:
13
- *
14
- * - Parsing the three `\c` argument shapes — positional (`db user host
15
- * port`, with `-` meaning "keep current"), URI (`postgresql://…`), and
16
- * conninfo key=value pairs (`dbname=foo host=bar`).
17
- * - Building a fresh {@link ConnectOptions} by merging the override with
18
- * the previous connection's opts and dispatching `PgConnection.connect`
19
- * via an injectable seam so tests can stub the wire layer.
20
- * - `\conninfo` — renders the PostgreSQL 18 "Connection Information"
21
- * two-column table (Parameter / Value), mirroring upstream
22
- * `exec_command_conninfo` in `src/bin/psql/command.c`. PG 18 rewrote
23
- * `\conninfo` from the old one-line "You are connected to …" message
24
- * into this table. Every value comes from the live connection's
25
- * accessors (`getConnectionInfo()`, `getTlsInfo()`,
26
- * `parameterStatus()`) — we issue NO SQL (the old form's
27
- * `inet_server_addr()` returned a bogus internal IP behind a proxy like
28
- * Neon). The ResultSet is handed to the active `\pset format` printer
29
- * so `\conninfo` honours `-A`, `-H`, etc. just like a query result.
30
- * - `\encoding` — like cmd_format.ts but additionally issues
31
- * `SET client_encoding TO …` on the live connection so the backend
32
- * ParameterStatus stays in sync with `settings.popt.topt.encoding`.
33
- * - `\password` — prompts for a password (twice), encodes it as a
34
- * SCRAM-SHA-256 verifier (RFC 5803 / PG's `ALTER USER … PASSWORD`
35
- * format), and issues the ALTER USER on the live connection. The
36
- * encoder lives in {@link scramSha256Verifier}; tests call it directly
37
- * with a fixed salt so we don't have to mock crypto.randomBytes.
38
- * Refuses to prompt when `settings.notty` is true (matches the spirit
39
- * of upstream — we don't yet wire `/dev/tty`, so notty would otherwise
40
- * fight the mainloop's stdin reader).
41
- *
42
- * What this module does NOT own:
43
- *
44
- * - Cataloguing the current database/user. These are populated by the
45
- * startup WP into psql vars DBNAME/USER (and kept in sync by `\c`), so
46
- * `\conninfo` reads them out of `settings.vars`, falling back to the
47
- * connect-opts surfaced on the live connection. Host/port/hostaddr and
48
- * the SSL facts come straight from the connection accessors.
49
- *
50
- * Password retention: `PgConnection` exposes the password captured at
51
- * connect time via a read-only `password` getter (mirroring libpq's
52
- * retention on the `PGconn`). `\c` reads it via a structural cast so we
53
- * don't have to widen the frozen {@link Connection} interface, and feeds
54
- * it to {@link mergeConnectOpts} so a reconnect to a different database
55
- * works without re-prompting. A new password supplied in the conninfo /
56
- * URI override always wins.
57
- */
58
- import { Buffer } from "node:buffer";
59
- import { createHash, createHmac, randomBytes as nodeRandomBytes, pbkdf2Sync, } from "node:crypto";
1
+ import { writeErr, writeOut } from "./shared.js";
60
2
  import { syncConnectionVars } from "../core/syncVars.js";
61
- import { readLine as readInputLine } from "../io/input.js";
3
+ import { readLine } from "../io/input.js";
62
4
  import { alignedPrinter } from "../print/aligned.js";
63
5
  import { asciidocPrinter } from "../print/asciidoc.js";
64
6
  import { csvPrinter } from "../print/csv.js";
@@ -68,753 +10,604 @@ import { latexLongtablePrinter, latexPrinter } from "../print/latex.js";
68
10
  import { troffMsPrinter } from "../print/troff.js";
69
11
  import { unalignedPrinter } from "../print/unaligned.js";
70
12
  import { PgConnection } from "../wire/connection.js";
71
- import { writeErr, writeOut } from "./shared.js";
72
- const defaultDeps = {
73
- connect: (opts) => PgConnection.connect(opts),
74
- readLine: defaultReadLine,
75
- randomBytes: nodeRandomBytes,
13
+ import { createHash, createHmac, pbkdf2Sync, randomBytes } from "node:crypto";
14
+ import { Buffer } from "node:buffer";
15
+ //#region src/psql/command/cmd_connect.ts
16
+ /**
17
+ * Connection backslash commands.
18
+ *
19
+ * TypeScript port of the corresponding `exec_command_*` functions in
20
+ * upstream PostgreSQL's `src/bin/psql/command.c`:
21
+ *
22
+ * - `\c` / `\connect` → exec_command_connect / do_connect
23
+ * - `\conninfo` → exec_command_conninfo
24
+ * - `\encoding` → exec_command_encoding
25
+ * - `\password` → exec_command_password
26
+ *
27
+ * What this module owns:
28
+ *
29
+ * - Parsing the three `\c` argument shapes — positional (`db user host
30
+ * port`, with `-` meaning "keep current"), URI (`postgresql://…`), and
31
+ * conninfo key=value pairs (`dbname=foo host=bar`).
32
+ * - Building a fresh {@link ConnectOptions} by merging the override with
33
+ * the previous connection's opts and dispatching `PgConnection.connect`
34
+ * via an injectable seam so tests can stub the wire layer.
35
+ * - `\conninfo` — renders the PostgreSQL 18 "Connection Information"
36
+ * two-column table (Parameter / Value), mirroring upstream
37
+ * `exec_command_conninfo` in `src/bin/psql/command.c`. PG 18 rewrote
38
+ * `\conninfo` from the old one-line "You are connected to …" message
39
+ * into this table. Every value comes from the live connection's
40
+ * accessors (`getConnectionInfo()`, `getTlsInfo()`,
41
+ * `parameterStatus()`) — we issue NO SQL (the old form's
42
+ * `inet_server_addr()` returned a bogus internal IP behind a proxy like
43
+ * Neon). The ResultSet is handed to the active `\pset format` printer
44
+ * so `\conninfo` honours `-A`, `-H`, etc. just like a query result.
45
+ * - `\encoding` — like cmd_format.ts but additionally issues
46
+ * `SET client_encoding TO …` on the live connection so the backend
47
+ * ParameterStatus stays in sync with `settings.popt.topt.encoding`.
48
+ * - `\password` — prompts for a password (twice), encodes it as a
49
+ * SCRAM-SHA-256 verifier (RFC 5803 / PG's `ALTER USER … PASSWORD`
50
+ * format), and issues the ALTER USER on the live connection. The
51
+ * encoder lives in {@link scramSha256Verifier}; tests call it directly
52
+ * with a fixed salt so we don't have to mock crypto.randomBytes.
53
+ * Refuses to prompt when `settings.notty` is true (matches the spirit
54
+ * of upstream — we don't yet wire `/dev/tty`, so notty would otherwise
55
+ * fight the mainloop's stdin reader).
56
+ *
57
+ * What this module does NOT own:
58
+ *
59
+ * - Cataloguing the current database/user. These are populated by the
60
+ * startup WP into psql vars DBNAME/USER (and kept in sync by `\c`), so
61
+ * `\conninfo` reads them out of `settings.vars`, falling back to the
62
+ * connect-opts surfaced on the live connection. Host/port/hostaddr and
63
+ * the SSL facts come straight from the connection accessors.
64
+ *
65
+ * Password retention: `PgConnection` exposes the password captured at
66
+ * connect time via a read-only `password` getter (mirroring libpq's
67
+ * retention on the `PGconn`). `\c` reads it via a structural cast so we
68
+ * don't have to widen the frozen {@link Connection} interface, and feeds
69
+ * it to {@link mergeConnectOpts} so a reconnect to a different database
70
+ * works without re-prompting. A new password supplied in the conninfo /
71
+ * URI override always wins.
72
+ */
73
+ let currentDeps = {
74
+ connect: (opts) => PgConnection.connect(opts),
75
+ readLine: defaultReadLine,
76
+ randomBytes
76
77
  };
77
- let currentDeps = defaultDeps;
78
78
  /** Override the module's runtime deps. Returns a restore function. */
79
- export const setCmdConnectDeps = (overrides) => {
80
- const prev = currentDeps;
81
- currentDeps = { ...prev, ...overrides };
82
- return () => {
83
- currentDeps = prev;
84
- };
79
+ const setCmdConnectDeps = (overrides) => {
80
+ const prev = currentDeps;
81
+ currentDeps = {
82
+ ...prev,
83
+ ...overrides
84
+ };
85
+ return () => {
86
+ currentDeps = prev;
87
+ };
85
88
  };
86
89
  const readConnectionPassword = (conn) => {
87
- if (!conn)
88
- return null;
89
- const raw = conn.password;
90
- return typeof raw === "string" && raw.length > 0 ? raw : null;
90
+ if (!conn) return null;
91
+ const raw = conn.password;
92
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
91
93
  };
92
94
  /**
93
- * Read the live connection's EFFECTIVE {@link ConnectOptions} (the full set
94
- * it was dialled with — sslmode, sslrootcert/cert/key/crl, sslnegotiation,
95
- * channelBinding, requireAuth, hostaddr, …). The {@link Connection} interface
96
- * deliberately hides these, but {@link PgConnection} keeps them on a (TS-)
97
- * private `opts` field; we read it structurally, the same pattern as
98
- * {@link readConnectionPassword}. Used to SEED a `\c` reconnect so it doesn't
99
- * silently drop TLS/cert options or downgrade sslmode.
100
- * Returns `null` for a mock/absent connection.
101
- */
95
+ * Read the live connection's EFFECTIVE {@link ConnectOptions} (the full set
96
+ * it was dialled with — sslmode, sslrootcert/cert/key/crl, sslnegotiation,
97
+ * channelBinding, requireAuth, hostaddr, …). The {@link Connection} interface
98
+ * deliberately hides these, but {@link PgConnection} keeps them on a (TS-)
99
+ * private `opts` field; we read it structurally, the same pattern as
100
+ * {@link readConnectionPassword}. Used to SEED a `\c` reconnect so it doesn't
101
+ * silently drop TLS/cert options or downgrade sslmode.
102
+ * Returns `null` for a mock/absent connection.
103
+ */
102
104
  const readConnectionOptions = (conn) => {
103
- if (!conn)
104
- return null;
105
- const raw = conn.opts;
106
- return raw !== undefined && raw !== null && typeof raw === "object"
107
- ? raw
108
- : null;
105
+ if (!conn) return null;
106
+ const raw = conn.opts;
107
+ return raw !== void 0 && raw !== null && typeof raw === "object" ? raw : null;
109
108
  };
110
- // ---------------------------------------------------------------------------
111
- // \c / \connect
112
- // ---------------------------------------------------------------------------
113
109
  const KEEP = "-";
114
110
  /**
115
- * Parse the argument tail of `\c` into a partial override of
116
- * {@link ConnectOptions}. Three forms:
117
- *
118
- * 1. **URI**: starts with `postgresql://` or `postgres://`.
119
- * 2. **conninfo**: contains `=` in the first token — treated as a space
120
- * separated `key=value` sequence (libpq's syntax; we don't implement
121
- * quoted values because psql itself only forwards what the user typed
122
- * to libpq's `PQconninfoParse`).
123
- * 3. **positional**: `[db [user [host [port]]]]`. Each can be `-` to keep
124
- * the current value.
125
- *
126
- * Returns an object with only the keys the user supplied. The caller is
127
- * responsible for merging with the previous opts.
128
- */
129
- export const parseConnectArgs = (rawArgs) => {
130
- const trimmed = rawArgs.trim();
131
- if (trimmed.length === 0)
132
- return {};
133
- // URI form.
134
- if (trimmed.startsWith("postgresql://") ||
135
- trimmed.startsWith("postgres://")) {
136
- return parseUri(trimmed);
137
- }
138
- // conninfo form — at least one token contains `=` (and that `=` is not
139
- // inside a quoted SQL value).
140
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed) ||
141
- /\s[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
142
- return parseConninfo(trimmed);
143
- }
144
- // Positional. Tokenise by whitespace; `-` is a sentinel.
145
- const tokens = trimmed.split(/\s+/);
146
- const out = {};
147
- if (tokens.length >= 1 && tokens[0] !== KEEP)
148
- out.database = tokens[0];
149
- if (tokens.length >= 2 && tokens[1] !== KEEP)
150
- out.user = tokens[1];
151
- if (tokens.length >= 3 && tokens[2] !== KEEP)
152
- out.host = tokens[2];
153
- if (tokens.length >= 4 && tokens[3] !== KEEP) {
154
- const port = parseInt(tokens[3], 10);
155
- if (!Number.isFinite(port) || port <= 0 || port > 65535) {
156
- return { error: `invalid port number "${tokens[3]}"` };
157
- }
158
- out.port = port;
159
- }
160
- return out;
111
+ * Parse the argument tail of `\c` into a partial override of
112
+ * {@link ConnectOptions}. Three forms:
113
+ *
114
+ * 1. **URI**: starts with `postgresql://` or `postgres://`.
115
+ * 2. **conninfo**: contains `=` in the first token — treated as a space
116
+ * separated `key=value` sequence (libpq's syntax; we don't implement
117
+ * quoted values because psql itself only forwards what the user typed
118
+ * to libpq's `PQconninfoParse`).
119
+ * 3. **positional**: `[db [user [host [port]]]]`. Each can be `-` to keep
120
+ * the current value.
121
+ *
122
+ * Returns an object with only the keys the user supplied. The caller is
123
+ * responsible for merging with the previous opts.
124
+ */
125
+ const parseConnectArgs = (rawArgs) => {
126
+ const trimmed = rawArgs.trim();
127
+ if (trimmed.length === 0) return {};
128
+ if (trimmed.startsWith("postgresql://") || trimmed.startsWith("postgres://")) return parseUri(trimmed);
129
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed) || /\s[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) return parseConninfo(trimmed);
130
+ const tokens = trimmed.split(/\s+/);
131
+ const out = {};
132
+ if (tokens.length >= 1 && tokens[0] !== KEEP) out.database = tokens[0];
133
+ if (tokens.length >= 2 && tokens[1] !== KEEP) out.user = tokens[1];
134
+ if (tokens.length >= 3 && tokens[2] !== KEEP) out.host = tokens[2];
135
+ if (tokens.length >= 4 && tokens[3] !== KEEP) {
136
+ const port = parseInt(tokens[3], 10);
137
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: `invalid port number "${tokens[3]}"` };
138
+ out.port = port;
139
+ }
140
+ return out;
161
141
  };
162
142
  /**
163
- * Apply a single libpq-style connection keyword to `out`, validating values
164
- * where psql does. `key` must already be lowercased. Returns `{ error }` on a
165
- * bad value, or `null` on success. Shared by the `key=value` conninfo parser
166
- * and the `\c <uri>` query-string parser so both honour the same keywords.
167
- */
143
+ * Apply a single libpq-style connection keyword to `out`, validating values
144
+ * where psql does. `key` must already be lowercased. Returns `{ error }` on a
145
+ * bad value, or `null` on success. Shared by the `key=value` conninfo parser
146
+ * and the `\c <uri>` query-string parser so both honour the same keywords.
147
+ */
168
148
  const applyConnInfoKey = (out, key, value) => {
169
- switch (key) {
170
- case "host":
171
- out.host = value;
172
- break;
173
- case "hostaddr":
174
- // Distinct from `host`: hostaddr is the literal IP to dial, while the
175
- // cert/SNI is still verified against `host`.
176
- out.hostaddr = value;
177
- break;
178
- case "port": {
179
- const port = parseInt(value, 10);
180
- if (!Number.isFinite(port) || port <= 0 || port > 65535) {
181
- return { error: `invalid port "${value}"` };
182
- }
183
- out.port = port;
184
- break;
185
- }
186
- case "user":
187
- out.user = value;
188
- break;
189
- case "password":
190
- out.password = value;
191
- break;
192
- case "dbname":
193
- case "database":
194
- out.database = value;
195
- break;
196
- case "application_name":
197
- out.applicationName = value;
198
- break;
199
- case "sslmode": {
200
- const allowed = [
201
- "disable",
202
- "allow",
203
- "prefer",
204
- "require",
205
- "verify-ca",
206
- "verify-full",
207
- ];
208
- if (!allowed.includes(value)) {
209
- return { error: `invalid sslmode "${value}"` };
210
- }
211
- out.ssl = value;
212
- break;
213
- }
214
- case "channel_binding": {
215
- const allowed = [
216
- "disable",
217
- "prefer",
218
- "require",
219
- ];
220
- if (!allowed.includes(value)) {
221
- return { error: `invalid channel_binding "${value}"` };
222
- }
223
- out.channelBinding = value;
224
- break;
225
- }
226
- case "client_encoding":
227
- out.clientEncoding = value;
228
- break;
229
- case "connect_timeout": {
230
- const n = parseInt(value, 10);
231
- if (!Number.isFinite(n) || n < 0) {
232
- return { error: `invalid connect_timeout "${value}"` };
233
- }
234
- out.connectTimeoutMs = n * 1000;
235
- break;
236
- }
237
- case "options":
238
- out.options = value;
239
- break;
240
- default:
241
- // Unknown keys are silently ignored — matches libpq's permissiveness
242
- // for forward-compat with future PG releases. We could warn here,
243
- // but psql itself doesn't.
244
- break;
245
- }
246
- return null;
149
+ switch (key) {
150
+ case "host":
151
+ out.host = value;
152
+ break;
153
+ case "hostaddr":
154
+ out.hostaddr = value;
155
+ break;
156
+ case "port": {
157
+ const port = parseInt(value, 10);
158
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: `invalid port "${value}"` };
159
+ out.port = port;
160
+ break;
161
+ }
162
+ case "user":
163
+ out.user = value;
164
+ break;
165
+ case "password":
166
+ out.password = value;
167
+ break;
168
+ case "dbname":
169
+ case "database":
170
+ out.database = value;
171
+ break;
172
+ case "application_name":
173
+ out.applicationName = value;
174
+ break;
175
+ case "sslmode":
176
+ if (![
177
+ "disable",
178
+ "allow",
179
+ "prefer",
180
+ "require",
181
+ "verify-ca",
182
+ "verify-full"
183
+ ].includes(value)) return { error: `invalid sslmode "${value}"` };
184
+ out.ssl = value;
185
+ break;
186
+ case "channel_binding":
187
+ if (![
188
+ "disable",
189
+ "prefer",
190
+ "require"
191
+ ].includes(value)) return { error: `invalid channel_binding "${value}"` };
192
+ out.channelBinding = value;
193
+ break;
194
+ case "client_encoding":
195
+ out.clientEncoding = value;
196
+ break;
197
+ case "connect_timeout": {
198
+ const n = parseInt(value, 10);
199
+ if (!Number.isFinite(n) || n < 0) return { error: `invalid connect_timeout "${value}"` };
200
+ out.connectTimeoutMs = n * 1e3;
201
+ break;
202
+ }
203
+ case "options": out.options = value;
204
+ }
205
+ return null;
247
206
  };
248
207
  const parseUri = (raw) => {
249
- let url;
250
- try {
251
- // Node's URL parser accepts `postgresql://` schemes.
252
- url = new URL(raw);
253
- }
254
- catch (err) {
255
- const msg = err instanceof Error ? err.message : String(err);
256
- return { error: `invalid URI: ${msg}` };
257
- }
258
- const out = {};
259
- // decodeURIComponent throws URIError on malformed `%` escapes (e.g. `%zz`);
260
- // wrap every decode so a bad URI surfaces as a clean error rather than an
261
- // unhandled exception that aborts the REPL.
262
- try {
263
- if (url.hostname.length > 0)
264
- out.host = decodeURIComponent(url.hostname);
265
- if (url.port.length > 0) {
266
- const port = parseInt(url.port, 10);
267
- if (!Number.isFinite(port) || port <= 0 || port > 65535) {
268
- return { error: `invalid port in URI` };
269
- }
270
- out.port = port;
271
- }
272
- if (url.username.length > 0)
273
- out.user = decodeURIComponent(url.username);
274
- if (url.password.length > 0) {
275
- out.password = decodeURIComponent(url.password);
276
- }
277
- const pathname = url.pathname.replace(/^\//, "");
278
- if (pathname.length > 0)
279
- out.database = decodeURIComponent(pathname);
280
- // Map the query string (?sslmode=require&connect_timeout=10&…) onto the
281
- // same connection keywords libpq accepts in a URI.
282
- for (const [rawKey, rawVal] of url.searchParams) {
283
- const res = applyConnInfoKey(out, rawKey.toLowerCase(), rawVal);
284
- if (res !== null)
285
- return res;
286
- }
287
- }
288
- catch (err) {
289
- const msg = err instanceof Error ? err.message : String(err);
290
- return { error: `invalid URI: ${msg}` };
291
- }
292
- return out;
208
+ let url;
209
+ try {
210
+ url = new URL(raw);
211
+ } catch (err) {
212
+ return { error: `invalid URI: ${err instanceof Error ? err.message : String(err)}` };
213
+ }
214
+ const out = {};
215
+ try {
216
+ if (url.hostname.length > 0) out.host = decodeURIComponent(url.hostname);
217
+ if (url.port.length > 0) {
218
+ const port = parseInt(url.port, 10);
219
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: `invalid port in URI` };
220
+ out.port = port;
221
+ }
222
+ if (url.username.length > 0) out.user = decodeURIComponent(url.username);
223
+ if (url.password.length > 0) out.password = decodeURIComponent(url.password);
224
+ const pathname = url.pathname.replace(/^\//, "");
225
+ if (pathname.length > 0) out.database = decodeURIComponent(pathname);
226
+ for (const [rawKey, rawVal] of url.searchParams) {
227
+ const res = applyConnInfoKey(out, rawKey.toLowerCase(), rawVal);
228
+ if (res !== null) return res;
229
+ }
230
+ } catch (err) {
231
+ return { error: `invalid URI: ${err instanceof Error ? err.message : String(err)}` };
232
+ }
233
+ return out;
293
234
  };
294
235
  const parseConninfo = (raw) => {
295
- const out = {};
296
- // Simple key=value tokenizer — splits on whitespace not inside single
297
- // quotes. libpq supports backslash-escapes inside quotes; we accept them
298
- // verbatim for now (none of the keys we care about typically need them).
299
- const pairs = [];
300
- let current = "";
301
- let inQuote = false;
302
- for (let i = 0; i < raw.length; i++) {
303
- const c = raw[i];
304
- if (inQuote) {
305
- if (c === "\\" && i + 1 < raw.length) {
306
- current += raw[i + 1];
307
- i++;
308
- continue;
309
- }
310
- if (c === "'") {
311
- inQuote = false;
312
- continue;
313
- }
314
- current += c;
315
- continue;
316
- }
317
- if (c === "'") {
318
- inQuote = true;
319
- continue;
320
- }
321
- if (/\s/.test(c)) {
322
- if (current.length > 0) {
323
- pairs.push(current);
324
- current = "";
325
- }
326
- continue;
327
- }
328
- current += c;
329
- }
330
- if (current.length > 0)
331
- pairs.push(current);
332
- for (const pair of pairs) {
333
- const eq = pair.indexOf("=");
334
- if (eq < 0) {
335
- return { error: `missing "=" after "${pair}" in connection info` };
336
- }
337
- const key = pair.slice(0, eq).toLowerCase();
338
- const value = pair.slice(eq + 1);
339
- const res = applyConnInfoKey(out, key, value);
340
- if (res !== null)
341
- return res;
342
- }
343
- return out;
236
+ const out = {};
237
+ const pairs = [];
238
+ let current = "";
239
+ let inQuote = false;
240
+ for (let i = 0; i < raw.length; i++) {
241
+ const c = raw[i];
242
+ if (inQuote) {
243
+ if (c === "\\" && i + 1 < raw.length) {
244
+ current += raw[i + 1];
245
+ i++;
246
+ continue;
247
+ }
248
+ if (c === "'") {
249
+ inQuote = false;
250
+ continue;
251
+ }
252
+ current += c;
253
+ continue;
254
+ }
255
+ if (c === "'") {
256
+ inQuote = true;
257
+ continue;
258
+ }
259
+ if (/\s/.test(c)) {
260
+ if (current.length > 0) {
261
+ pairs.push(current);
262
+ current = "";
263
+ }
264
+ continue;
265
+ }
266
+ current += c;
267
+ }
268
+ if (current.length > 0) pairs.push(current);
269
+ for (const pair of pairs) {
270
+ const eq = pair.indexOf("=");
271
+ if (eq < 0) return { error: `missing "=" after "${pair}" in connection info` };
272
+ const key = pair.slice(0, eq).toLowerCase();
273
+ const value = pair.slice(eq + 1);
274
+ const res = applyConnInfoKey(out, key, value);
275
+ if (res !== null) return res;
276
+ }
277
+ return out;
344
278
  };
345
279
  /**
346
- * Build a full {@link ConnectOptions} for the new connection by merging
347
- * `override` over the "previous" connection state. The previous values are
348
- * sourced from `settings.vars` (HOST/PORT/USER/DBNAME/PASSWORD) which the
349
- * startup WP populates; for fields not represented there (sslmode, etc.)
350
- * we use safe defaults.
351
- *
352
- * For passwords specifically the precedence is:
353
- * 1. `override.password` — anything the user typed in this `\c` invocation
354
- * (URI password or `password=` conninfo key) wins.
355
- * 2. `previousPassword` — the password captured on the live
356
- * {@link PgConnection}, mirroring libpq's behaviour of retaining the
357
- * credential on the `PGconn` so reconnects work transparently.
358
- * 3. `PASSWORD` psql var — set by `-W` / `PGPASSWORD` at startup.
359
- *
360
- * Returns `null` if the merge can't produce a usable opts (e.g. no database
361
- * and no current connection).
362
- */
363
- export const mergeConnectOpts = (settings, override, prior = null) => {
364
- const vars = settings.vars;
365
- // Overlay only the keys the user actually supplied (drop `undefined`s so a
366
- // spread doesn't clobber a seeded value with `undefined`).
367
- const ov = {};
368
- for (const [k, v] of Object.entries(override)) {
369
- if (v !== undefined)
370
- ov[k] = v;
371
- }
372
- // libpq's do_connect clones the prior connection's full conninfo and
373
- // overrides only user-specified keys — so a reconnect keeps sslmode,
374
- // sslrootcert/cert/key/crl, sslnegotiation, channelBinding, requireAuth,
375
- // hostaddr, etc. Seed from the live connection's effective options; fall
376
- // back to {} when there is none.
377
- const seed = prior ? { ...prior } : {};
378
- // libpq clears keep_password when user, host, OR port changes, so a stored
379
- // credential isn't transmitted to a different principal/server. Compare the
380
- // override against the prior live target.
381
- const hostChanged = ov.host !== undefined && ov.host !== prior?.host;
382
- const targetChanged = hostChanged ||
383
- (ov.port !== undefined && ov.port !== prior?.port) ||
384
- (ov.user !== undefined && ov.user !== prior?.user);
385
- if (targetChanged)
386
- delete seed.password;
387
- // A new host invalidates the prior hostaddr (it was the old host's IP).
388
- if (hostChanged)
389
- delete seed.hostaddr;
390
- const merged = { ...seed, ...ov };
391
- // Fill the required fields from vars / env / defaults when neither the
392
- // override nor the prior connection supplied them.
393
- const host = merged.host ?? vars.get("HOST") ?? "localhost";
394
- const portStr = vars.get("PORT");
395
- const port = merged.port ?? (portStr !== undefined ? parseInt(portStr, 10) : 5432);
396
- if (!Number.isFinite(port) || port <= 0 || port > 65535) {
397
- return { error: `invalid port number` };
398
- }
399
- const user = merged.user ??
400
- vars.get("USER") ??
401
- process.env.USER ??
402
- process.env.USERNAME ??
403
- "";
404
- if (user.length === 0) {
405
- return { error: "no user name specified" };
406
- }
407
- const database = merged.database ?? vars.get("DBNAME") ?? user;
408
- const password = merged.password ?? vars.get("PASSWORD");
409
- const ssl = merged.ssl ?? "prefer";
410
- return {
411
- ...merged,
412
- host,
413
- port,
414
- user,
415
- password: password ?? undefined,
416
- database,
417
- ssl,
418
- applicationName: merged.applicationName ?? vars.get("APPLICATION_NAME"),
419
- clientEncoding: merged.clientEncoding ?? settings.popt.topt.encoding,
420
- };
280
+ * Build a full {@link ConnectOptions} for the new connection by merging
281
+ * `override` over the "previous" connection state. The previous values are
282
+ * sourced from `settings.vars` (HOST/PORT/USER/DBNAME/PASSWORD) which the
283
+ * startup WP populates; for fields not represented there (sslmode, etc.)
284
+ * we use safe defaults.
285
+ *
286
+ * For passwords specifically the precedence is:
287
+ * 1. `override.password` — anything the user typed in this `\c` invocation
288
+ * (URI password or `password=` conninfo key) wins.
289
+ * 2. `previousPassword` — the password captured on the live
290
+ * {@link PgConnection}, mirroring libpq's behaviour of retaining the
291
+ * credential on the `PGconn` so reconnects work transparently.
292
+ * 3. `PASSWORD` psql var — set by `-W` / `PGPASSWORD` at startup.
293
+ *
294
+ * Returns `null` if the merge can't produce a usable opts (e.g. no database
295
+ * and no current connection).
296
+ */
297
+ const mergeConnectOpts = (settings, override, prior = null) => {
298
+ const vars = settings.vars;
299
+ const ov = {};
300
+ for (const [k, v] of Object.entries(override)) if (v !== void 0) ov[k] = v;
301
+ const seed = prior ? { ...prior } : {};
302
+ const hostChanged = ov.host !== void 0 && ov.host !== prior?.host;
303
+ if (hostChanged || ov.port !== void 0 && ov.port !== prior?.port || ov.user !== void 0 && ov.user !== prior?.user) delete seed.password;
304
+ if (hostChanged) delete seed.hostaddr;
305
+ const merged = {
306
+ ...seed,
307
+ ...ov
308
+ };
309
+ const host = merged.host ?? vars.get("HOST") ?? "localhost";
310
+ const portStr = vars.get("PORT");
311
+ const port = merged.port ?? (portStr !== void 0 ? parseInt(portStr, 10) : 5432);
312
+ if (!Number.isFinite(port) || port <= 0 || port > 65535) return { error: `invalid port number` };
313
+ const user = merged.user ?? vars.get("USER") ?? process.env.USER ?? process.env.USERNAME ?? "";
314
+ if (user.length === 0) return { error: "no user name specified" };
315
+ const database = merged.database ?? vars.get("DBNAME") ?? user;
316
+ const password = merged.password ?? vars.get("PASSWORD");
317
+ const ssl = merged.ssl ?? "prefer";
318
+ return {
319
+ ...merged,
320
+ host,
321
+ port,
322
+ user,
323
+ password: password ?? void 0,
324
+ database,
325
+ ssl,
326
+ applicationName: merged.applicationName ?? vars.get("APPLICATION_NAME"),
327
+ clientEncoding: merged.clientEncoding ?? settings.popt.topt.encoding
328
+ };
421
329
  };
422
330
  /** `\c` / `\connect` — reconnect, possibly to a different database/user/host. */
423
- export const cmdConnect = {
424
- name: "c",
425
- aliases: ["connect"],
426
- argMode: "whole-line",
427
- helpKey: "c",
428
- run: async (ctx) => {
429
- const rawArgs = ctx.restOfLine();
430
- if (rawArgs.trim().length === 0) {
431
- // No args → print conninfo, matching upstream `do_connect` short
432
- // circuit.
433
- return runConninfo(ctx);
434
- }
435
- const parsed = parseConnectArgs(rawArgs);
436
- if ("error" in parsed) {
437
- writeErr(`\\${ctx.cmdName}: ${parsed.error}\n`);
438
- return { status: "error" };
439
- }
440
- // Seed the reconnect from the live connection's EFFECTIVE options so TLS /
441
- // cert / auth settings survive and the prior password is only
442
- // reused when the principal/server is unchanged. Both are read
443
- // structurally from PgConnection (the frozen Connection interface hides
444
- // them). readConnectionPassword stays as the fallback for mocks/drivers
445
- // that expose `password` but not `opts`.
446
- const priorOpts = readConnectionOptions(ctx.settings.db);
447
- const priorPw = readConnectionPassword(ctx.settings.db);
448
- const prior = priorOpts ?? (priorPw !== null ? { password: priorPw } : null);
449
- const newOpts = mergeConnectOpts(ctx.settings, parsed, prior);
450
- if ("error" in newOpts) {
451
- writeErr(`\\${ctx.cmdName}: ${newOpts.error}\n`);
452
- return { status: "error" };
453
- }
454
- let next;
455
- try {
456
- next = await currentDeps.connect(newOpts);
457
- }
458
- catch (err) {
459
- // psql keeps the old connection on failure.
460
- const msg = err instanceof Error ? err.message : String(err);
461
- writeErr(`\\${ctx.cmdName}: connection failed: ${msg}\n`);
462
- return { status: "error" };
463
- }
464
- const old = ctx.settings.db;
465
- ctx.settings.db = next;
466
- // Mirror upstream do_connect → SyncVariables(): refresh the
467
- // connection-driven psql vars (DBNAME/USER/HOST/PORT/ENCODING/
468
- // SERVER_VERSION_*) from the new live connection so subsequent
469
- // `:DBNAME`/`:USER`/etc. interpolations reflect the reconnect target.
470
- syncConnectionVars(ctx.settings.vars, next);
471
- if (old && !old.isClosed()) {
472
- try {
473
- await old.close();
474
- }
475
- catch {
476
- // Old connection may already be in a weird state; we've moved on.
477
- }
478
- }
479
- writeOut(`You are now connected to database "${newOpts.database}" as user "${newOpts.user}".\n`);
480
- return { status: "ok" };
481
- },
331
+ const cmdConnect = {
332
+ name: "c",
333
+ aliases: ["connect"],
334
+ argMode: "whole-line",
335
+ helpKey: "c",
336
+ run: async (ctx) => {
337
+ const rawArgs = ctx.restOfLine();
338
+ if (rawArgs.trim().length === 0) return runConninfo(ctx);
339
+ const parsed = parseConnectArgs(rawArgs);
340
+ if ("error" in parsed) {
341
+ writeErr(`\\${ctx.cmdName}: ${parsed.error}\n`);
342
+ return { status: "error" };
343
+ }
344
+ const priorOpts = readConnectionOptions(ctx.settings.db);
345
+ const priorPw = readConnectionPassword(ctx.settings.db);
346
+ const prior = priorOpts ?? (priorPw !== null ? { password: priorPw } : null);
347
+ const newOpts = mergeConnectOpts(ctx.settings, parsed, prior);
348
+ if ("error" in newOpts) {
349
+ writeErr(`\\${ctx.cmdName}: ${newOpts.error}\n`);
350
+ return { status: "error" };
351
+ }
352
+ let next;
353
+ try {
354
+ next = await currentDeps.connect(newOpts);
355
+ } catch (err) {
356
+ const msg = err instanceof Error ? err.message : String(err);
357
+ writeErr(`\\${ctx.cmdName}: connection failed: ${msg}\n`);
358
+ return { status: "error" };
359
+ }
360
+ const old = ctx.settings.db;
361
+ ctx.settings.db = next;
362
+ syncConnectionVars(ctx.settings.vars, next);
363
+ if (old && !old.isClosed()) try {
364
+ await old.close();
365
+ } catch {}
366
+ writeOut(`You are now connected to database "${newOpts.database}" as user "${newOpts.user}".\n`);
367
+ return { status: "ok" };
368
+ }
482
369
  };
483
- // ---------------------------------------------------------------------------
484
- // \conninfo
485
- // ---------------------------------------------------------------------------
486
370
  /**
487
- * Pick the printer for the active output format. Mirrors the private
488
- * `pickPrinter` in `core/common.ts` / `pickActivePrinter` in `cmd_io.ts`
489
- * — replicated here (those are module-private) to avoid an import cycle,
490
- * the same established pattern this codebase already uses for the two
491
- * other copies.
492
- */
371
+ * Pick the printer for the active output format. Mirrors the private
372
+ * `pickPrinter` in `core/common.ts` / `pickActivePrinter` in `cmd_io.ts`
373
+ * — replicated here (those are module-private) to avoid an import cycle,
374
+ * the same established pattern this codebase already uses for the two
375
+ * other copies.
376
+ */
493
377
  const pickActivePrinter = (settings) => {
494
- switch (settings.popt.topt.format) {
495
- case "aligned":
496
- case "wrapped":
497
- return alignedPrinter;
498
- case "unaligned":
499
- return unalignedPrinter;
500
- case "csv":
501
- return csvPrinter;
502
- case "json":
503
- return jsonPrinter;
504
- case "html":
505
- return htmlPrinter;
506
- case "asciidoc":
507
- return asciidocPrinter;
508
- case "latex":
509
- return latexPrinter;
510
- case "latex-longtable":
511
- return latexLongtablePrinter;
512
- case "troff-ms":
513
- return troffMsPrinter;
514
- default:
515
- return alignedPrinter;
516
- }
378
+ switch (settings.popt.topt.format) {
379
+ case "aligned":
380
+ case "wrapped": return alignedPrinter;
381
+ case "unaligned": return unalignedPrinter;
382
+ case "csv": return csvPrinter;
383
+ case "json": return jsonPrinter;
384
+ case "html": return htmlPrinter;
385
+ case "asciidoc": return asciidocPrinter;
386
+ case "latex": return latexPrinter;
387
+ case "latex-longtable": return latexLongtablePrinter;
388
+ case "troff-ms": return troffMsPrinter;
389
+ default: return alignedPrinter;
390
+ }
517
391
  };
518
392
  /** A text-typed field descriptor for the synthetic conninfo ResultSet. */
519
393
  const textField = (name) => ({
520
- name,
521
- tableID: 0,
522
- columnID: 0,
523
- // OID 25 = `text`: left-aligned, like both columns of upstream's output.
524
- dataTypeID: 25,
525
- dataTypeSize: -1,
526
- dataTypeModifier: -1,
527
- format: 0,
394
+ name,
395
+ tableID: 0,
396
+ columnID: 0,
397
+ dataTypeID: 25,
398
+ dataTypeSize: -1,
399
+ dataTypeModifier: -1,
400
+ format: 0
528
401
  });
529
402
  /**
530
- * Build the (Parameter, Value) rows for the PG18 connection-information
531
- * table. Mirrors the row order and gating of upstream
532
- * `exec_command_conninfo` (`src/bin/psql/command.c`).
533
- */
403
+ * Build the (Parameter, Value) rows for the PG18 connection-information
404
+ * table. Mirrors the row order and gating of upstream
405
+ * `exec_command_conninfo` (`src/bin/psql/command.c`).
406
+ */
534
407
  const buildConninfoRows = (ctx, db) => {
535
- const info = db.getConnectionInfo?.() ?? null;
536
- const tls = db.getTlsInfo?.() ?? null;
537
- // Database / user come from the psql vars the startup WP populates (and
538
- // that `\c` keeps in sync); fall back to the connect-opts surfaced on the
539
- // connection when a var is missing.
540
- const database = ctx.settings.vars.get("DBNAME") ??
541
- db.database ??
542
- "";
543
- const user = ctx.settings.vars.get("USER") ??
544
- db.user ??
545
- "";
546
- const host = info?.host ?? ctx.settings.vars.get("HOST") ?? "";
547
- const hostaddr = info?.hostaddr ?? null;
548
- const port = info?.port ?? Number(ctx.settings.vars.get("PORT") ?? Number.NaN);
549
- const portStr = Number.isFinite(port) ? String(port) : "";
550
- const rows = [];
551
- rows.push(["Database", database]);
552
- rows.push(["Client User", user]);
553
- // Host rows. A Unix-domain socket path (starts with '/') prints a
554
- // "Socket Directory" (or "Host Address" when a hostaddr was fixed);
555
- // otherwise "Host", plus a separate "Host Address" only when a distinct
556
- // hostaddr is present.
557
- if (host.startsWith("/")) {
558
- if (hostaddr !== null && hostaddr.length > 0) {
559
- rows.push(["Host Address", hostaddr]);
560
- }
561
- else {
562
- rows.push(["Socket Directory", host]);
563
- }
564
- }
565
- else {
566
- rows.push(["Host", host]);
567
- if (hostaddr !== null && hostaddr.length > 0 && hostaddr !== host) {
568
- rows.push(["Host Address", hostaddr]);
569
- }
570
- }
571
- rows.push(["Server Port", portStr]);
572
- rows.push(["Options", info?.options ?? ""]);
573
- rows.push(["Protocol Version", "3.0"]);
574
- rows.push(["Password Used", info?.passwordUsed ? "true" : "false"]);
575
- rows.push(["GSSAPI Authenticated", info?.gssapiUsed ? "true" : "false"]);
576
- rows.push(["Backend PID", String(info?.backendPid ?? 0)]);
577
- rows.push(["SSL Connection", tls ? "true" : "false"]);
578
- if (tls) {
579
- rows.push(["SSL Library", tls.library]);
580
- rows.push(["SSL Protocol", tls.protocol]);
581
- rows.push([
582
- "SSL Key Bits",
583
- tls.keyBits !== null ? String(tls.keyBits) : "",
584
- ]);
585
- rows.push(["SSL Cipher", tls.cipher]);
586
- rows.push([
587
- "SSL Compression",
588
- tls.compression !== "off" ? "true" : "false",
589
- ]);
590
- rows.push([
591
- "ALPN",
592
- tls.alpn && tls.alpn.length > 0 ? tls.alpn : "none",
593
- ]);
594
- }
595
- rows.push([
596
- "Superuser",
597
- ctx.settings.db?.parameterStatus("is_superuser") ?? "unknown",
598
- ]);
599
- rows.push([
600
- "Hot Standby",
601
- ctx.settings.db?.parameterStatus("in_hot_standby") ?? "unknown",
602
- ]);
603
- return rows;
408
+ const info = db.getConnectionInfo?.() ?? null;
409
+ const tls = db.getTlsInfo?.() ?? null;
410
+ const database = ctx.settings.vars.get("DBNAME") ?? db.database ?? "";
411
+ const user = ctx.settings.vars.get("USER") ?? db.user ?? "";
412
+ const host = info?.host ?? ctx.settings.vars.get("HOST") ?? "";
413
+ const hostaddr = info?.hostaddr ?? null;
414
+ const port = info?.port ?? Number(ctx.settings.vars.get("PORT") ?? NaN);
415
+ const portStr = Number.isFinite(port) ? String(port) : "";
416
+ const rows = [];
417
+ rows.push(["Database", database]);
418
+ rows.push(["Client User", user]);
419
+ if (host.startsWith("/")) if (hostaddr !== null && hostaddr.length > 0) rows.push(["Host Address", hostaddr]);
420
+ else rows.push(["Socket Directory", host]);
421
+ else {
422
+ rows.push(["Host", host]);
423
+ if (hostaddr !== null && hostaddr.length > 0 && hostaddr !== host) rows.push(["Host Address", hostaddr]);
424
+ }
425
+ rows.push(["Server Port", portStr]);
426
+ rows.push(["Options", info?.options ?? ""]);
427
+ rows.push(["Protocol Version", "3.0"]);
428
+ rows.push(["Password Used", info?.passwordUsed ? "true" : "false"]);
429
+ rows.push(["GSSAPI Authenticated", info?.gssapiUsed ? "true" : "false"]);
430
+ rows.push(["Backend PID", String(info?.backendPid ?? 0)]);
431
+ rows.push(["SSL Connection", tls ? "true" : "false"]);
432
+ if (tls) {
433
+ rows.push(["SSL Library", tls.library]);
434
+ rows.push(["SSL Protocol", tls.protocol]);
435
+ rows.push(["SSL Key Bits", tls.keyBits !== null ? String(tls.keyBits) : ""]);
436
+ rows.push(["SSL Cipher", tls.cipher]);
437
+ rows.push(["SSL Compression", tls.compression !== "off" ? "true" : "false"]);
438
+ rows.push(["ALPN", tls.alpn && tls.alpn.length > 0 ? tls.alpn : "none"]);
439
+ }
440
+ rows.push(["Superuser", ctx.settings.db?.parameterStatus("is_superuser") ?? "unknown"]);
441
+ rows.push(["Hot Standby", ctx.settings.db?.parameterStatus("in_hot_standby") ?? "unknown"]);
442
+ return rows;
604
443
  };
605
444
  const runConninfo = async (ctx) => {
606
- const db = ctx.settings.db;
607
- if (!db) {
608
- writeOut("You are currently not connected to a database.\n");
609
- return { status: "ok" };
610
- }
611
- const rows = buildConninfoRows(ctx, db);
612
- const rs = {
613
- command: "SELECT",
614
- rowCount: rows.length,
615
- oid: null,
616
- fields: [textField("Parameter"), textField("Value")],
617
- rows: rows.map(([p, v]) => [p, v]),
618
- notices: [],
619
- };
620
- // Render with the active printer (honours `\pset format`), titled
621
- // "Connection Information" with the default `(N rows)` footer left on,
622
- // matching PG18's `printQuery(... title = "Connection Information")`.
623
- const popt = {
624
- ...ctx.settings.popt,
625
- title: "Connection Information",
626
- topt: {
627
- ...ctx.settings.popt.topt,
628
- title: "Connection Information",
629
- defaultFooter: true,
630
- },
631
- };
632
- const out = process.stdout;
633
- await pickActivePrinter(ctx.settings).printQuery(rs, popt, out);
634
- return { status: "ok" };
445
+ const db = ctx.settings.db;
446
+ if (!db) {
447
+ writeOut("You are currently not connected to a database.\n");
448
+ return { status: "ok" };
449
+ }
450
+ const rows = buildConninfoRows(ctx, db);
451
+ const rs = {
452
+ command: "SELECT",
453
+ rowCount: rows.length,
454
+ oid: null,
455
+ fields: [textField("Parameter"), textField("Value")],
456
+ rows: rows.map(([p, v]) => [p, v]),
457
+ notices: []
458
+ };
459
+ const popt = {
460
+ ...ctx.settings.popt,
461
+ title: "Connection Information",
462
+ topt: {
463
+ ...ctx.settings.popt.topt,
464
+ title: "Connection Information",
465
+ defaultFooter: true
466
+ }
467
+ };
468
+ const out = process.stdout;
469
+ await pickActivePrinter(ctx.settings).printQuery(rs, popt, out);
470
+ return { status: "ok" };
635
471
  };
636
472
  /** `\conninfo` — print info about the current connection. */
637
- export const cmdConninfo = {
638
- name: "conninfo",
639
- helpKey: "conninfo",
640
- run: (ctx) => runConninfo(ctx),
473
+ const cmdConninfo = {
474
+ name: "conninfo",
475
+ helpKey: "conninfo",
476
+ run: (ctx) => runConninfo(ctx)
641
477
  };
642
- // ---------------------------------------------------------------------------
643
- // \encoding — overrides cmd_format.ts's version so the live connection
644
- // also sees the new client_encoding (via SET client_encoding TO …).
645
- // ---------------------------------------------------------------------------
646
478
  /** `\encoding [NAME]` — show or set the client encoding, propagating to the connection. */
647
- export const cmdEncoding = {
648
- name: "encoding",
649
- helpKey: "encoding",
650
- run: async (ctx) => {
651
- const arg = ctx.nextArg("normal");
652
- const topt = ctx.settings.popt.topt;
653
- if (arg === null) {
654
- writeOut(`${topt.encoding}\n`);
655
- return { status: "ok" };
656
- }
657
- const db = ctx.settings.db;
658
- if (db && !db.isClosed()) {
659
- try {
660
- // PG accepts both quoted and unquoted encoding names; we use the
661
- // quoted form for safety. Encoding names are ASCII identifiers.
662
- await db.execSimple(`SET client_encoding TO ${db.escapeLiteral(arg)}`);
663
- }
664
- catch (err) {
665
- const msg = err instanceof Error ? err.message : String(err);
666
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
667
- return { status: "error" };
668
- }
669
- }
670
- topt.encoding = arg;
671
- ctx.settings.vars.set("ENCODING", arg);
672
- return { status: "ok" };
673
- },
479
+ const cmdEncoding = {
480
+ name: "encoding",
481
+ helpKey: "encoding",
482
+ run: async (ctx) => {
483
+ const arg = ctx.nextArg("normal");
484
+ const topt = ctx.settings.popt.topt;
485
+ if (arg === null) {
486
+ writeOut(`${topt.encoding}\n`);
487
+ return { status: "ok" };
488
+ }
489
+ const db = ctx.settings.db;
490
+ if (db && !db.isClosed()) try {
491
+ await db.execSimple(`SET client_encoding TO ${db.escapeLiteral(arg)}`);
492
+ } catch (err) {
493
+ const msg = err instanceof Error ? err.message : String(err);
494
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
495
+ return { status: "error" };
496
+ }
497
+ topt.encoding = arg;
498
+ ctx.settings.vars.set("ENCODING", arg);
499
+ return { status: "ok" };
500
+ }
674
501
  };
675
- // ---------------------------------------------------------------------------
676
- // \password
677
- // ---------------------------------------------------------------------------
678
502
  /**
679
- * Encode a password into PostgreSQL's SCRAM-SHA-256 verifier format, the
680
- * value `ALTER ROLE … PASSWORD '…'` expects when the server is configured
681
- * with `password_encryption = scram-sha-256` (the default since PG 14):
682
- *
683
- * SCRAM-SHA-256$<iterations>:<base64 salt>$<base64 storedKey>:<base64 serverKey>
684
- *
685
- * Derivation follows RFC 5802 §3:
686
- *
687
- * SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
688
- * ClientKey = HMAC-SHA256(SaltedPassword, "Client Key")
689
- * StoredKey = SHA256(ClientKey)
690
- * ServerKey = HMAC-SHA256(SaltedPassword, "Server Key")
691
- *
692
- * Sending the encoded form (rather than the plaintext password) means the
693
- * server never sees the password, even briefly, and the SCRAM verifier is
694
- * stored as-is in `pg_authid.rolpassword`.
695
- *
696
- * Iterations default to 4096 to match PG's `scram_iterations` default and
697
- * libpq's `PQchangePassword`. The caller can override (lower for tests,
698
- * higher for stronger hardening).
699
- */
700
- export const scramSha256Verifier = (password, salt, iterations = 4096) => {
701
- const saltedPassword = pbkdf2Sync(Buffer.from(password, "utf8"), salt, iterations, 32, "sha256");
702
- const clientKey = createHmac("sha256", saltedPassword)
703
- .update("Client Key")
704
- .digest();
705
- const storedKey = createHash("sha256").update(clientKey).digest();
706
- const serverKey = createHmac("sha256", saltedPassword)
707
- .update("Server Key")
708
- .digest();
709
- return ("SCRAM-SHA-256$" +
710
- String(iterations) +
711
- ":" +
712
- salt.toString("base64") +
713
- "$" +
714
- storedKey.toString("base64") +
715
- ":" +
716
- serverKey.toString("base64"));
503
+ * Encode a password into PostgreSQL's SCRAM-SHA-256 verifier format, the
504
+ * value `ALTER ROLE … PASSWORD '…'` expects when the server is configured
505
+ * with `password_encryption = scram-sha-256` (the default since PG 14):
506
+ *
507
+ * SCRAM-SHA-256$<iterations>:<base64 salt>$<base64 storedKey>:<base64 serverKey>
508
+ *
509
+ * Derivation follows RFC 5802 §3:
510
+ *
511
+ * SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
512
+ * ClientKey = HMAC-SHA256(SaltedPassword, "Client Key")
513
+ * StoredKey = SHA256(ClientKey)
514
+ * ServerKey = HMAC-SHA256(SaltedPassword, "Server Key")
515
+ *
516
+ * Sending the encoded form (rather than the plaintext password) means the
517
+ * server never sees the password, even briefly, and the SCRAM verifier is
518
+ * stored as-is in `pg_authid.rolpassword`.
519
+ *
520
+ * Iterations default to 4096 to match PG's `scram_iterations` default and
521
+ * libpq's `PQchangePassword`. The caller can override (lower for tests,
522
+ * higher for stronger hardening).
523
+ */
524
+ const scramSha256Verifier = (password, salt, iterations = 4096) => {
525
+ const saltedPassword = pbkdf2Sync(Buffer.from(password, "utf8"), salt, iterations, 32, "sha256");
526
+ const clientKey = createHmac("sha256", saltedPassword).update("Client Key").digest();
527
+ const storedKey = createHash("sha256").update(clientKey).digest();
528
+ const serverKey = createHmac("sha256", saltedPassword).update("Server Key").digest();
529
+ return "SCRAM-SHA-256$" + String(iterations) + ":" + salt.toString("base64") + "$" + storedKey.toString("base64") + ":" + serverKey.toString("base64");
717
530
  };
718
531
  /**
719
- * Default password / prompt reader. Delegates to the shared input layer
720
- * ({@link readInputLine}), which suppresses echo on a TTY and falls back to a
721
- * plain line read otherwise. Kept as a named function so the test seam in
722
- * {@link CmdConnectDeps} can swap it out.
723
- */
532
+ * Default password / prompt reader. Delegates to the shared input layer
533
+ * ({@link readInputLine}), which suppresses echo on a TTY and falls back to a
534
+ * plain line read otherwise. Kept as a named function so the test seam in
535
+ * {@link CmdConnectDeps} can swap it out.
536
+ */
724
537
  function defaultReadLine(prompt, opts) {
725
- return readInputLine(prompt, { echo: opts.echo });
538
+ return readLine(prompt, { echo: opts.echo });
726
539
  }
727
540
  /** `\password [USERNAME]` — change a role's password, locally hashed as SCRAM. */
728
- export const cmdPassword = {
729
- name: "password",
730
- helpKey: "password",
731
- run: async (ctx) => {
732
- const db = ctx.settings.db;
733
- if (!db || db.isClosed()) {
734
- writeErr(`\\${ctx.cmdName}: not connected\n`);
735
- return { status: "error" };
736
- }
737
- // Refuse to prompt for a password when stdin is not a TTY. Upstream uses
738
- // /dev/tty as a fallback so it works even with piped stdin; we don't yet
739
- // wire that path, so notty is a hard error (it would otherwise corrupt
740
- // the mainloop's own readline iterator on stdin). Tests mock the prompt
741
- // via `readLine` and run with `notty: false`, the `defaultSettings`
742
- // default.
743
- if (ctx.settings.notty) {
744
- writeErr(`\\${ctx.cmdName}: not in interactive mode\n`);
745
- return { status: "error" };
746
- }
747
- const userArg = ctx.nextArg("sql-id");
748
- let user = userArg;
749
- if (user === null) {
750
- try {
751
- const rs = await db.query("SELECT CURRENT_USER");
752
- const row = rs.rows[0] ?? [];
753
- user = typeof row[0] === "string" ? row[0] : null;
754
- }
755
- catch (err) {
756
- const msg = err instanceof Error ? err.message : String(err);
757
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
758
- return { status: "error" };
759
- }
760
- if (!user) {
761
- writeErr(`\\${ctx.cmdName}: could not determine current user\n`);
762
- return { status: "error" };
763
- }
764
- }
765
- let pw1;
766
- let pw2;
767
- try {
768
- pw1 = await currentDeps.readLine(`Enter new password for user "${user}": `, { echo: false });
769
- pw2 = await currentDeps.readLine("Enter it again: ", {
770
- echo: false,
771
- });
772
- }
773
- catch (err) {
774
- const msg = err instanceof Error ? err.message : String(err);
775
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
776
- return { status: "error" };
777
- }
778
- if (pw1 !== pw2) {
779
- writeErr(`\\${ctx.cmdName}: Passwords didn't match.\n`);
780
- return { status: "error" };
781
- }
782
- if (pw1.length === 0) {
783
- writeErr(`\\${ctx.cmdName}: empty password\n`);
784
- return { status: "error" };
785
- }
786
- // Match upstream's PQchangePassword: `ALTER USER <id> PASSWORD <lit>`.
787
- // (ALTER USER and ALTER ROLE are synonyms in PG, but we follow the
788
- // libpq spelling for byte-for-byte parity with vanilla psql.)
789
- const salt = currentDeps.randomBytes(16);
790
- const verifier = scramSha256Verifier(pw1, salt);
791
- const sql = "ALTER USER " +
792
- db.escapeIdentifier(user) +
793
- " PASSWORD " +
794
- db.escapeLiteral(verifier);
795
- try {
796
- await db.execSimple(sql);
797
- }
798
- catch (err) {
799
- const msg = err instanceof Error ? err.message : String(err);
800
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
801
- return { status: "error" };
802
- }
803
- return { status: "ok" };
804
- },
541
+ const cmdPassword = {
542
+ name: "password",
543
+ helpKey: "password",
544
+ run: async (ctx) => {
545
+ const db = ctx.settings.db;
546
+ if (!db || db.isClosed()) {
547
+ writeErr(`\\${ctx.cmdName}: not connected\n`);
548
+ return { status: "error" };
549
+ }
550
+ if (ctx.settings.notty) {
551
+ writeErr(`\\${ctx.cmdName}: not in interactive mode\n`);
552
+ return { status: "error" };
553
+ }
554
+ let user = ctx.nextArg("sql-id");
555
+ if (user === null) {
556
+ try {
557
+ const row = (await db.query("SELECT CURRENT_USER")).rows[0] ?? [];
558
+ user = typeof row[0] === "string" ? row[0] : null;
559
+ } catch (err) {
560
+ const msg = err instanceof Error ? err.message : String(err);
561
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
562
+ return { status: "error" };
563
+ }
564
+ if (!user) {
565
+ writeErr(`\\${ctx.cmdName}: could not determine current user\n`);
566
+ return { status: "error" };
567
+ }
568
+ }
569
+ let pw1;
570
+ let pw2;
571
+ try {
572
+ pw1 = await currentDeps.readLine(`Enter new password for user "${user}": `, { echo: false });
573
+ pw2 = await currentDeps.readLine("Enter it again: ", { echo: false });
574
+ } catch (err) {
575
+ const msg = err instanceof Error ? err.message : String(err);
576
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
577
+ return { status: "error" };
578
+ }
579
+ if (pw1 !== pw2) {
580
+ writeErr(`\\${ctx.cmdName}: Passwords didn't match.\n`);
581
+ return { status: "error" };
582
+ }
583
+ if (pw1.length === 0) {
584
+ writeErr(`\\${ctx.cmdName}: empty password\n`);
585
+ return { status: "error" };
586
+ }
587
+ const salt = currentDeps.randomBytes(16);
588
+ const verifier = scramSha256Verifier(pw1, salt);
589
+ const sql = "ALTER USER " + db.escapeIdentifier(user) + " PASSWORD " + db.escapeLiteral(verifier);
590
+ try {
591
+ await db.execSimple(sql);
592
+ } catch (err) {
593
+ const msg = err instanceof Error ? err.message : String(err);
594
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
595
+ return { status: "error" };
596
+ }
597
+ return { status: "ok" };
598
+ }
805
599
  };
806
- // ---------------------------------------------------------------------------
807
- // Registry hook
808
- // ---------------------------------------------------------------------------
809
600
  /**
810
- * Register the four connection commands on the given registry. Called from
811
- * `dispatch.ts::defaultRegistry()`. Re-registering an existing primary name
812
- * overrides — that's how we replace the no-op `\encoding` from cmd_format
813
- * with this WP's version that propagates to the connection.
814
- */
815
- export const registerConnectCommands = (registry) => {
816
- registry.register(cmdConnect);
817
- registry.register(cmdConninfo);
818
- registry.register(cmdEncoding);
819
- registry.register(cmdPassword);
601
+ * Register the four connection commands on the given registry. Called from
602
+ * `dispatch.ts::defaultRegistry()`. Re-registering an existing primary name
603
+ * overrides — that's how we replace the no-op `\encoding` from cmd_format
604
+ * with this WP's version that propagates to the connection.
605
+ */
606
+ const registerConnectCommands = (registry) => {
607
+ registry.register(cmdConnect);
608
+ registry.register(cmdConninfo);
609
+ registry.register(cmdEncoding);
610
+ registry.register(cmdPassword);
820
611
  };
612
+ //#endregion
613
+ export { cmdConnect, cmdConninfo, cmdEncoding, cmdPassword, mergeConnectOpts, parseConnectArgs, registerConnectCommands, scramSha256Verifier, setCmdConnectDeps };