neon 3.0.0 → 3.1.1

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 (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -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 };