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,728 +1,598 @@
1
- /**
2
- * Meta backslash commands.
3
- *
4
- * TypeScript port of the corresponding `exec_command_*` functions in
5
- * upstream PostgreSQL's `src/bin/psql/command.c`:
6
- *
7
- * - `\q` / `\quit` → exec_command_quit
8
- * - `\r` / `\reset` → exec_command_reset
9
- * - `\!` → exec_command_shell_escape (do_shell)
10
- * - `\cd` → exec_command_cd
11
- * - `\echo`, `\qecho`, `\warn` → exec_command_echo / qecho / warn
12
- * - `\prompt` → exec_command_prompt
13
- * - `\set`, `\unset` → exec_command_set / exec_command_unset
14
- * - `\getenv`, `\setenv` → exec_command_getenv / exec_command_setenv
15
- * - `\errverbose` → exec_command_errverbose
16
- * - `\timing` → exec_command_timing
17
- * - `\copyright` → exec_command_copyright
18
- * - `\h` / `\help` → exec_command_help (helpSQL)
19
- *
20
- * Each command is exported as a `BackslashCmdSpec` so {@link defaultRegistry}
21
- * in `dispatch.ts` can register them. Error messages follow upstream's
22
- * `\<cmd>: <message>` shape and go to stderr; on failure we return
23
- * `{ status: 'error' }`. Successful invocations return `{ status: 'ok' }`.
24
- *
25
- * Stubs / deferred behaviour:
26
- *
27
- * - `\!` always returns `{ status: 'ok' }` — upstream does not propagate
28
- * the child's exit status to the surrounding script, only the run-mode.
29
- * Tests use a stdio mock; in interactive use the child inherits stdio.
30
- * - `\prompt -` (no-echo password prompting) reads via the shared input
31
- * layer with echo suppressed on a TTY (falling back to a plain read on
32
- * non-interactive input).
33
- * - `\qecho` writes to `settings.logfile` if set, else stdout. Upstream
34
- * additionally honours a separate "query output" file set via `\o`;
35
- * that wiring lives in WP-15 and we leave the hook in place.
36
- */
37
- import { spawnSync } from "node:child_process";
38
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
39
- import { tmpdir } from "node:os";
40
- import { join } from "node:path";
1
+ import { parseBool, writeErr, writeOut } from "./shared.js";
2
+ import { readLine } from "../io/input.js";
41
3
  import { helpSQL, slashUsage } from "../core/help.js";
42
4
  import { getHistory } from "../io/history.js";
43
- import { readLine } from "../io/input.js";
44
- import { parseBool, writeErr, writeOut } from "./shared.js";
5
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { spawnSync } from "node:child_process";
9
+ //#region src/psql/command/cmd_meta.ts
10
+ /**
11
+ * Meta backslash commands.
12
+ *
13
+ * TypeScript port of the corresponding `exec_command_*` functions in
14
+ * upstream PostgreSQL's `src/bin/psql/command.c`:
15
+ *
16
+ * - `\q` / `\quit` → exec_command_quit
17
+ * - `\r` / `\reset` → exec_command_reset
18
+ * - `\!` → exec_command_shell_escape (do_shell)
19
+ * - `\cd` → exec_command_cd
20
+ * - `\echo`, `\qecho`, `\warn` → exec_command_echo / qecho / warn
21
+ * - `\prompt` → exec_command_prompt
22
+ * - `\set`, `\unset` → exec_command_set / exec_command_unset
23
+ * - `\getenv`, `\setenv` → exec_command_getenv / exec_command_setenv
24
+ * - `\errverbose` → exec_command_errverbose
25
+ * - `\timing` → exec_command_timing
26
+ * - `\copyright` → exec_command_copyright
27
+ * - `\h` / `\help` → exec_command_help (helpSQL)
28
+ *
29
+ * Each command is exported as a `BackslashCmdSpec` so {@link defaultRegistry}
30
+ * in `dispatch.ts` can register them. Error messages follow upstream's
31
+ * `\<cmd>: <message>` shape and go to stderr; on failure we return
32
+ * `{ status: 'error' }`. Successful invocations return `{ status: 'ok' }`.
33
+ *
34
+ * Stubs / deferred behaviour:
35
+ *
36
+ * - `\!` always returns `{ status: 'ok' }` — upstream does not propagate
37
+ * the child's exit status to the surrounding script, only the run-mode.
38
+ * Tests use a stdio mock; in interactive use the child inherits stdio.
39
+ * - `\prompt -` (no-echo password prompting) reads via the shared input
40
+ * layer with echo suppressed on a TTY (falling back to a plain read on
41
+ * non-interactive input).
42
+ * - `\qecho` writes to `settings.logfile` if set, else stdout. Upstream
43
+ * additionally honours a separate "query output" file set via `\o`;
44
+ * that wiring lives in WP-15 and we leave the hook in place.
45
+ */
45
46
  /** `\q` / `\quit` — exit the REPL. */
46
- export const cmdQuit = {
47
- name: "q",
48
- aliases: ["quit"],
49
- helpKey: "q",
50
- run: () => Promise.resolve({ status: "exit" }),
47
+ const cmdQuit = {
48
+ name: "q",
49
+ aliases: ["quit"],
50
+ helpKey: "q",
51
+ run: () => Promise.resolve({ status: "exit" })
51
52
  };
52
53
  /**
53
- * `\!` — shell escape. Whole-line mode: the entire rest of the line is the
54
- * command string.
55
- *
56
- * - `\!` (no args) → spawn `$SHELL -i` (fallback `sh -i`)
57
- * - `\!command args` → spawn `sh -c 'command args'`
58
- *
59
- * In both cases the child inherits stdio and we return `{ status: 'ok' }`
60
- * regardless of the child's exit status — matching upstream `do_shell`,
61
- * which keeps the REPL alive after a failing shell command rather than
62
- * propagating the exit code. Catching a spawn-time exception keeps us
63
- * resilient against environments where `sh` is unavailable.
64
- */
65
- export const cmdShell = {
66
- name: "!",
67
- argMode: "whole-line",
68
- helpKey: "!",
69
- run: (ctx) => {
70
- const line = ctx.restOfLine().trim();
71
- try {
72
- if (line.length === 0) {
73
- const shell = process.env.SHELL ?? "/bin/sh";
74
- spawnSync(shell, ["-i"], { stdio: "inherit" });
75
- }
76
- else {
77
- spawnSync("sh", ["-c", line], { stdio: "inherit" });
78
- }
79
- }
80
- catch {
81
- // Upstream `do_shell` swallows shell-spawn failures: the REPL has to
82
- // keep running even when the child won't start. Status stays `ok` so
83
- // a failing `\!` is purely informational.
84
- }
85
- return Promise.resolve({ status: "ok" });
86
- },
54
+ * `\!` — shell escape. Whole-line mode: the entire rest of the line is the
55
+ * command string.
56
+ *
57
+ * - `\!` (no args) → spawn `$SHELL -i` (fallback `sh -i`)
58
+ * - `\!command args` → spawn `sh -c 'command args'`
59
+ *
60
+ * In both cases the child inherits stdio and we return `{ status: 'ok' }`
61
+ * regardless of the child's exit status — matching upstream `do_shell`,
62
+ * which keeps the REPL alive after a failing shell command rather than
63
+ * propagating the exit code. Catching a spawn-time exception keeps us
64
+ * resilient against environments where `sh` is unavailable.
65
+ */
66
+ const cmdShell = {
67
+ name: "!",
68
+ argMode: "whole-line",
69
+ helpKey: "!",
70
+ run: (ctx) => {
71
+ const line = ctx.restOfLine().trim();
72
+ try {
73
+ if (line.length === 0) {
74
+ const shell = process.env.SHELL ?? "/bin/sh";
75
+ spawnSync(shell, ["-i"], { stdio: "inherit" });
76
+ } else spawnSync("sh", ["-c", line], { stdio: "inherit" });
77
+ } catch {}
78
+ return Promise.resolve({ status: "ok" });
79
+ }
87
80
  };
88
81
  /** `\cd [dir]` — change cwd. No arg falls back to `$HOME`. */
89
- export const cmdCd = {
90
- name: "cd",
91
- helpKey: "cd",
92
- run: (ctx) => {
93
- const dir = ctx.nextArg("normal");
94
- const target = dir && dir.length > 0 ? dir : (process.env.HOME ?? null);
95
- if (!target) {
96
- writeErr(`\\${ctx.cmdName}: could not determine home directory\n`);
97
- return Promise.resolve({ status: "error" });
98
- }
99
- try {
100
- process.chdir(target);
101
- return Promise.resolve({ status: "ok" });
102
- }
103
- catch (err) {
104
- const msg = err instanceof Error ? err.message : String(err);
105
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
106
- return Promise.resolve({ status: "error" });
107
- }
108
- },
82
+ const cmdCd = {
83
+ name: "cd",
84
+ helpKey: "cd",
85
+ run: (ctx) => {
86
+ const dir = ctx.nextArg("normal");
87
+ const target = dir && dir.length > 0 ? dir : process.env.HOME ?? null;
88
+ if (!target) {
89
+ writeErr(`\\${ctx.cmdName}: could not determine home directory\n`);
90
+ return Promise.resolve({ status: "error" });
91
+ }
92
+ try {
93
+ process.chdir(target);
94
+ return Promise.resolve({ status: "ok" });
95
+ } catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
98
+ return Promise.resolve({ status: "error" });
99
+ }
100
+ }
109
101
  };
110
102
  /**
111
- * Helper for `\echo` / `\qecho` / `\warn`. Reads args until exhausted,
112
- * honours the leading `-n` flag (suppresses trailing newline), joins with
113
- * single spaces, and writes to the chosen stream.
114
- *
115
- * Upstream `exec_command_echo` only treats `-n` as a flag when the source
116
- * was the unquoted two-character token `-n`. `'-n'` (single-quoted) is a
117
- * literal value: it should be printed AND the trailing newline kept. We
118
- * inspect `ctx.rawArgs` directly because `nextArg` discards quote
119
- * metadata after lexing.
120
- */
103
+ * Helper for `\echo` / `\qecho` / `\warn`. Reads args until exhausted,
104
+ * honours the leading `-n` flag (suppresses trailing newline), joins with
105
+ * single spaces, and writes to the chosen stream.
106
+ *
107
+ * Upstream `exec_command_echo` only treats `-n` as a flag when the source
108
+ * was the unquoted two-character token `-n`. `'-n'` (single-quoted) is a
109
+ * literal value: it should be printed AND the trailing newline kept. We
110
+ * inspect `ctx.rawArgs` directly because `nextArg` discards quote
111
+ * metadata after lexing.
112
+ */
121
113
  const runEcho = (ctx, write) => {
122
- const parts = [];
123
- let noNewline = false;
124
- let first = true;
125
- // Pre-scan the raw text to decide whether the first arg was the
126
- // unquoted `-n` token. We can't rely on the lexed arg value alone:
127
- // `'-n'` / `"-n"` produce the same string but must be treated as data.
128
- const firstArgIsUnquotedDashN = (() => {
129
- let i = 0;
130
- while (i < ctx.rawArgs.length && /\s/.test(ctx.rawArgs[i]))
131
- i++;
132
- return (ctx.rawArgs.slice(i, i + 2) === "-n" &&
133
- (i + 2 === ctx.rawArgs.length || /\s/.test(ctx.rawArgs[i + 2])));
134
- })();
135
- for (;;) {
136
- const arg = ctx.nextArg("normal");
137
- if (arg === null)
138
- break;
139
- if (first && firstArgIsUnquotedDashN && arg === "-n") {
140
- noNewline = true;
141
- first = false;
142
- continue;
143
- }
144
- first = false;
145
- parts.push(arg);
146
- }
147
- const out = parts.join(" ") + (noNewline ? "" : "\n");
148
- write(out);
149
- return { status: "ok" };
114
+ const parts = [];
115
+ let noNewline = false;
116
+ let first = true;
117
+ const firstArgIsUnquotedDashN = (() => {
118
+ let i = 0;
119
+ while (i < ctx.rawArgs.length && /\s/.test(ctx.rawArgs[i])) i++;
120
+ return ctx.rawArgs.slice(i, i + 2) === "-n" && (i + 2 === ctx.rawArgs.length || /\s/.test(ctx.rawArgs[i + 2]));
121
+ })();
122
+ for (;;) {
123
+ const arg = ctx.nextArg("normal");
124
+ if (arg === null) break;
125
+ if (first && firstArgIsUnquotedDashN && arg === "-n") {
126
+ noNewline = true;
127
+ first = false;
128
+ continue;
129
+ }
130
+ first = false;
131
+ parts.push(arg);
132
+ }
133
+ write(parts.join(" ") + (noNewline ? "" : "\n"));
134
+ return { status: "ok" };
150
135
  };
151
136
  /** `\echo` — write args to stdout. */
152
- export const cmdEcho = {
153
- name: "echo",
154
- helpKey: "echo",
155
- run: (ctx) => Promise.resolve(runEcho(ctx, writeOut)),
137
+ const cmdEcho = {
138
+ name: "echo",
139
+ helpKey: "echo",
140
+ run: (ctx) => Promise.resolve(runEcho(ctx, writeOut))
156
141
  };
157
142
  /** `\qecho` — write args to the query output (logfile if set, else stdout). */
158
- export const cmdQecho = {
159
- name: "qecho",
160
- helpKey: "qecho",
161
- run: (ctx) => {
162
- const { logfile } = ctx.settings;
163
- const write = (s) => {
164
- if (logfile) {
165
- logfile.write(s);
166
- }
167
- else {
168
- writeOut(s);
169
- }
170
- };
171
- return Promise.resolve(runEcho(ctx, write));
172
- },
143
+ const cmdQecho = {
144
+ name: "qecho",
145
+ helpKey: "qecho",
146
+ run: (ctx) => {
147
+ const { logfile } = ctx.settings;
148
+ const write = (s) => {
149
+ if (logfile) logfile.write(s);
150
+ else writeOut(s);
151
+ };
152
+ return Promise.resolve(runEcho(ctx, write));
153
+ }
173
154
  };
174
155
  /** `\warn` — write args to stderr. */
175
- export const cmdWarn = {
176
- name: "warn",
177
- helpKey: "warn",
178
- run: (ctx) => Promise.resolve(runEcho(ctx, writeErr)),
156
+ const cmdWarn = {
157
+ name: "warn",
158
+ helpKey: "warn",
159
+ run: (ctx) => Promise.resolve(runEcho(ctx, writeErr))
179
160
  };
180
161
  /**
181
- * `\prompt [TEXT] varname`
182
- *
183
- * Upstream: read one line of input from the terminal, optionally with a
184
- * prompt prefix, and assign it to a psql variable. A leading `-` flag
185
- * requests a no-echo read (used for password prompts); we honour it by
186
- * reading through the shared input layer with echo suppressed on a TTY
187
- * (and a plain read otherwise — non-interactive input still consumes the
188
- * line, matching upstream).
189
- *
190
- * Args after the optional `-` flag are `[TEXT] varname`: if only one
191
- * remains it is the variable name and no prompt prefix is shown; if two,
192
- * the first is the prompt and the second the variable.
193
- */
194
- export const cmdPrompt = {
195
- name: "prompt",
196
- helpKey: "prompt",
197
- run: async (ctx) => {
198
- const args = [];
199
- for (;;) {
200
- const a = ctx.nextArg("normal");
201
- if (a === null)
202
- break;
203
- args.push(a);
204
- }
205
- // A leading `-` selects the no-echo (password) read path.
206
- let echo = true;
207
- if (args.length > 0 && args[0] === "-") {
208
- echo = false;
209
- args.shift();
210
- }
211
- if (args.length === 0) {
212
- writeErr(`\\${ctx.cmdName}: missing required argument\n`);
213
- return { status: "error" };
214
- }
215
- let promptText = "";
216
- let varname;
217
- if (args.length === 1) {
218
- varname = args[0];
219
- }
220
- else {
221
- promptText = args[0];
222
- varname = args[1];
223
- }
224
- const line = await readLine(promptText, { echo });
225
- if (!ctx.settings.vars.set(varname, line)) {
226
- writeErr(`\\${ctx.cmdName}: invalid variable name "${varname}"\n`);
227
- return { status: "error" };
228
- }
229
- return { status: "ok" };
230
- },
162
+ * `\prompt [TEXT] varname`
163
+ *
164
+ * Upstream: read one line of input from the terminal, optionally with a
165
+ * prompt prefix, and assign it to a psql variable. A leading `-` flag
166
+ * requests a no-echo read (used for password prompts); we honour it by
167
+ * reading through the shared input layer with echo suppressed on a TTY
168
+ * (and a plain read otherwise — non-interactive input still consumes the
169
+ * line, matching upstream).
170
+ *
171
+ * Args after the optional `-` flag are `[TEXT] varname`: if only one
172
+ * remains it is the variable name and no prompt prefix is shown; if two,
173
+ * the first is the prompt and the second the variable.
174
+ */
175
+ const cmdPrompt = {
176
+ name: "prompt",
177
+ helpKey: "prompt",
178
+ run: async (ctx) => {
179
+ const args = [];
180
+ for (;;) {
181
+ const a = ctx.nextArg("normal");
182
+ if (a === null) break;
183
+ args.push(a);
184
+ }
185
+ let echo = true;
186
+ if (args.length > 0 && args[0] === "-") {
187
+ echo = false;
188
+ args.shift();
189
+ }
190
+ if (args.length === 0) {
191
+ writeErr(`\\${ctx.cmdName}: missing required argument\n`);
192
+ return { status: "error" };
193
+ }
194
+ let promptText = "";
195
+ let varname;
196
+ if (args.length === 1) varname = args[0];
197
+ else {
198
+ promptText = args[0];
199
+ varname = args[1];
200
+ }
201
+ const line = await readLine(promptText, { echo });
202
+ if (!ctx.settings.vars.set(varname, line)) {
203
+ writeErr(`\\${ctx.cmdName}: invalid variable name "${varname}"\n`);
204
+ return { status: "error" };
205
+ }
206
+ return { status: "ok" };
207
+ }
231
208
  };
232
209
  /**
233
- * `\set [varname [value...]]`
234
- *
235
- * - No args → list all variables (sorted, `name = 'value'` per line) to
236
- * stdout. Upstream uses single-quotes around the value.
237
- * - One arg → set the variable to the empty string.
238
- * - More args → join the rest with a single space and set the variable.
239
- *
240
- * Diagnostics mirror upstream `exec_command_set` in `src/bin/psql/command.c`:
241
- *
242
- * - Names containing characters outside `[A-Za-z_][A-Za-z0-9_]*` produce
243
- * `invalid variable name: "<name>"` (prefixed with `psql: `).
244
- * - Per-variable hook rejections (AUTOCOMMIT / FETCH_COUNT /
245
- * ON_ERROR_ROLLBACK / VERBOSITY / etc.) carry the hook's message
246
- * verbatim; we add only the `psql: ` prefix.
247
- * - Hook vetoes with no message fall back to a generic line. This
248
- * should not happen in practice — every registered hook either
249
- * accepts or returns a wording string.
250
- */
251
- export const cmdSet = {
252
- name: "set",
253
- helpKey: "set",
254
- run: (ctx) => {
255
- const name = ctx.nextArg("normal");
256
- if (name === null) {
257
- // List all vars sorted by name.
258
- const entries = [...ctx.settings.vars.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
259
- for (const [k, v] of entries) {
260
- writeOut(`${k} = '${v}'\n`);
261
- }
262
- return Promise.resolve({ status: "ok" });
263
- }
264
- const values = [];
265
- for (;;) {
266
- const a = ctx.nextArg("normal");
267
- if (a === null)
268
- break;
269
- values.push(a);
270
- }
271
- const value = values.join("");
272
- const result = ctx.settings.vars.trySet(name, value);
273
- if (!result.ok) {
274
- const prefix = psqlErrorPrefix(ctx.settings);
275
- if (result.reason === "invalid-name") {
276
- writeErr(`${prefix}invalid variable name: "${name}"\n`);
277
- }
278
- else if (result.error !== undefined) {
279
- // Hook supplied its own wording — emit verbatim, prefixed with
280
- // `psql: `. The message intentionally does NOT carry a severity
281
- // (`error:` / `ERROR:`) because upstream's per-variable hooks
282
- // also emit just `psql: <msg>` (see `bool_substitute_hook` etc.).
283
- writeErr(`${prefix}${result.error}\n`);
284
- }
285
- else {
286
- // Hook returned `false` without a message — fall back to a
287
- // generic line so callers still see something. None of the
288
- // built-in hooks take this path, but third-party callers might.
289
- writeErr(`${prefix}error while setting variable "${name}"\n`);
290
- }
291
- return Promise.resolve({ status: "error" });
292
- }
293
- return Promise.resolve({ status: "ok" });
294
- },
210
+ * `\set [varname [value...]]`
211
+ *
212
+ * - No args → list all variables (sorted, `name = 'value'` per line) to
213
+ * stdout. Upstream uses single-quotes around the value.
214
+ * - One arg → set the variable to the empty string.
215
+ * - More args → join the rest with a single space and set the variable.
216
+ *
217
+ * Diagnostics mirror upstream `exec_command_set` in `src/bin/psql/command.c`:
218
+ *
219
+ * - Names containing characters outside `[A-Za-z_][A-Za-z0-9_]*` produce
220
+ * `invalid variable name: "<name>"` (prefixed with `psql: `).
221
+ * - Per-variable hook rejections (AUTOCOMMIT / FETCH_COUNT /
222
+ * ON_ERROR_ROLLBACK / VERBOSITY / etc.) carry the hook's message
223
+ * verbatim; we add only the `psql: ` prefix.
224
+ * - Hook vetoes with no message fall back to a generic line. This
225
+ * should not happen in practice — every registered hook either
226
+ * accepts or returns a wording string.
227
+ */
228
+ const cmdSet = {
229
+ name: "set",
230
+ helpKey: "set",
231
+ run: (ctx) => {
232
+ const name = ctx.nextArg("normal");
233
+ if (name === null) {
234
+ const entries = [...ctx.settings.vars.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
235
+ for (const [k, v] of entries) writeOut(`${k} = '${v}'\n`);
236
+ return Promise.resolve({ status: "ok" });
237
+ }
238
+ const values = [];
239
+ for (;;) {
240
+ const a = ctx.nextArg("normal");
241
+ if (a === null) break;
242
+ values.push(a);
243
+ }
244
+ const value = values.join("");
245
+ const result = ctx.settings.vars.trySet(name, value);
246
+ if (!result.ok) {
247
+ const prefix = psqlErrorPrefix(ctx.settings);
248
+ if (result.reason === "invalid-name") writeErr(`${prefix}invalid variable name: "${name}"\n`);
249
+ else if (result.error !== void 0) writeErr(`${prefix}${result.error}\n`);
250
+ else writeErr(`${prefix}error while setting variable "${name}"\n`);
251
+ return Promise.resolve({ status: "error" });
252
+ }
253
+ return Promise.resolve({ status: "ok" });
254
+ }
295
255
  };
296
256
  /**
297
- * `\r` / `\reset` — discard the accumulated query buffer.
298
- *
299
- * Mirrors upstream `exec_command_reset`:
300
- *
301
- * resetPQExpBuffer(query_buf);
302
- * psql_scan_reset(scan_state);
303
- * if (!pset.quiet)
304
- * puts(_("Query buffer reset (cleared)."));
305
- *
306
- * We model the buffer + scanner reset via `status: 'reset-buf'`; the
307
- * mainloop wipes `queryBuf` and re-initialises `scanState` when it sees
308
- * this. The diagnostic is gated on the `quiet` setting so `psql -q` (and
309
- * the regress harness, which passes `--quiet`) produces no output.
310
- */
311
- export const cmdReset = {
312
- name: "r",
313
- aliases: ["reset"],
314
- helpKey: "r",
315
- run: (ctx) => {
316
- if (!ctx.settings.quiet) {
317
- writeOut("Query buffer reset (cleared).\n");
318
- }
319
- return Promise.resolve({ status: "reset-buf", newBuf: "" });
320
- },
257
+ * `\r` / `\reset` — discard the accumulated query buffer.
258
+ *
259
+ * Mirrors upstream `exec_command_reset`:
260
+ *
261
+ * resetPQExpBuffer(query_buf);
262
+ * psql_scan_reset(scan_state);
263
+ * if (!pset.quiet)
264
+ * puts(_("Query buffer reset (cleared)."));
265
+ *
266
+ * We model the buffer + scanner reset via `status: 'reset-buf'`; the
267
+ * mainloop wipes `queryBuf` and re-initialises `scanState` when it sees
268
+ * this. The diagnostic is gated on the `quiet` setting so `psql -q` (and
269
+ * the regress harness, which passes `--quiet`) produces no output.
270
+ */
271
+ const cmdReset = {
272
+ name: "r",
273
+ aliases: ["reset"],
274
+ helpKey: "r",
275
+ run: (ctx) => {
276
+ if (!ctx.settings.quiet) writeOut("Query buffer reset (cleared).\n");
277
+ return Promise.resolve({
278
+ status: "reset-buf",
279
+ newBuf: ""
280
+ });
281
+ }
321
282
  };
322
283
  /** `\unset varname` — unset a psql variable. */
323
- export const cmdUnset = {
324
- name: "unset",
325
- helpKey: "unset",
326
- run: (ctx) => {
327
- const name = ctx.nextArg("normal");
328
- if (name === null) {
329
- writeErr(`\\${ctx.cmdName}: missing required argument\n`);
330
- return Promise.resolve({ status: "error" });
331
- }
332
- ctx.settings.vars.unset(name);
333
- return Promise.resolve({ status: "ok" });
334
- },
284
+ const cmdUnset = {
285
+ name: "unset",
286
+ helpKey: "unset",
287
+ run: (ctx) => {
288
+ const name = ctx.nextArg("normal");
289
+ if (name === null) {
290
+ writeErr(`\\${ctx.cmdName}: missing required argument\n`);
291
+ return Promise.resolve({ status: "error" });
292
+ }
293
+ ctx.settings.vars.unset(name);
294
+ return Promise.resolve({ status: "ok" });
295
+ }
335
296
  };
336
- export const cmdGetenv = {
337
- name: "getenv",
338
- helpKey: "getenv",
339
- run: (ctx) => {
340
- const varname = ctx.nextArg("normal");
341
- const envname = ctx.nextArg("normal");
342
- if (varname === null || envname === null) {
343
- writeErr(`\\${ctx.cmdName}: missing required argument\n`);
344
- return Promise.resolve({ status: "error" });
345
- }
346
- const value = process.env[envname];
347
- if (value === undefined) {
348
- return Promise.resolve({ status: "ok" });
349
- }
350
- if (!ctx.settings.vars.set(varname, value)) {
351
- writeErr(`\\${ctx.cmdName}: invalid variable name "${varname}"\n`);
352
- return Promise.resolve({ status: "error" });
353
- }
354
- return Promise.resolve({ status: "ok" });
355
- },
297
+ const cmdGetenv = {
298
+ name: "getenv",
299
+ helpKey: "getenv",
300
+ run: (ctx) => {
301
+ const varname = ctx.nextArg("normal");
302
+ const envname = ctx.nextArg("normal");
303
+ if (varname === null || envname === null) {
304
+ writeErr(`\\${ctx.cmdName}: missing required argument\n`);
305
+ return Promise.resolve({ status: "error" });
306
+ }
307
+ const value = process.env[envname];
308
+ if (value === void 0) return Promise.resolve({ status: "ok" });
309
+ if (!ctx.settings.vars.set(varname, value)) {
310
+ writeErr(`\\${ctx.cmdName}: invalid variable name "${varname}"\n`);
311
+ return Promise.resolve({ status: "error" });
312
+ }
313
+ return Promise.resolve({ status: "ok" });
314
+ }
356
315
  };
357
316
  /**
358
- * `\setenv envvar [value]`
359
- *
360
- * Set `process.env[envvar] = value`; with no value, delete it. Upstream
361
- * rejects names containing `=`.
362
- */
363
- export const cmdSetenv = {
364
- name: "setenv",
365
- helpKey: "setenv",
366
- run: (ctx) => {
367
- const envname = ctx.nextArg("normal");
368
- if (envname === null) {
369
- writeErr(`\\${ctx.cmdName}: missing required argument\n`);
370
- return Promise.resolve({ status: "error" });
371
- }
372
- if (envname.includes("=")) {
373
- writeErr(`\\${ctx.cmdName}: environment variable name must not contain "="\n`);
374
- return Promise.resolve({ status: "error" });
375
- }
376
- // Upstream `exec_command_setenv` reads BOTH the name AND the value with
377
- // OT_NORMAL — `:VAR` substitution applies to the value so
378
- // `\setenv FOO :BAR` propagates the psql-variable value into the env.
379
- // (Earlier 'no-vars' was a misread; vanilla psql expands inside the
380
- // value.) The mainloop context maintains a per-mode cursor, so using
381
- // a single mode for both calls also keeps positional reads in sync —
382
- // each cursor advances exactly once per call.
383
- const value = ctx.nextArg("normal");
384
- if (value === null) {
385
- delete process.env[envname];
386
- }
387
- else {
388
- process.env[envname] = value;
389
- }
390
- return Promise.resolve({ status: "ok" });
391
- },
317
+ * `\setenv envvar [value]`
318
+ *
319
+ * Set `process.env[envvar] = value`; with no value, delete it. Upstream
320
+ * rejects names containing `=`.
321
+ */
322
+ const cmdSetenv = {
323
+ name: "setenv",
324
+ helpKey: "setenv",
325
+ run: (ctx) => {
326
+ const envname = ctx.nextArg("normal");
327
+ if (envname === null) {
328
+ writeErr(`\\${ctx.cmdName}: missing required argument\n`);
329
+ return Promise.resolve({ status: "error" });
330
+ }
331
+ if (envname.includes("=")) {
332
+ writeErr(`\\${ctx.cmdName}: environment variable name must not contain "="\n`);
333
+ return Promise.resolve({ status: "error" });
334
+ }
335
+ const value = ctx.nextArg("normal");
336
+ if (value === null) delete process.env[envname];
337
+ else process.env[envname] = value;
338
+ return Promise.resolve({ status: "ok" });
339
+ }
392
340
  };
393
341
  /**
394
- * Build the `psql:` diagnostic prefix that upstream `pg_log_pre_callback`
395
- * prepends to error lines, but ONLY when reading from a script file. Mirrors:
396
- *
397
- * if (cur_cmd_source == QUERY_FROM_FILE)
398
- * fprintf(stderr, "psql:%s:%d: ", cur_cmd_filename, cur_cmd_lineno);
399
- *
400
- * - `curCmdSource === 'file'` (running under `-f FILE`, `\i FILE`,
401
- * `\ir FILE`, or `.psqlrc`): `psql:<inputfile>:<lineno>: `.
402
- * - Stdin pipe, `-c "..."`, interactive REPL: empty string — vanilla
403
- * `psql --no-psqlrc -X` reading SQL from stdin emits NO prefix on
404
- * either `\set` validation errors or server `ERROR:` lines.
405
- *
406
- * Returned string ends in a trailing space when non-empty so callers can
407
- * concatenate the severity directly (`prefix + 'ERROR: msg'`).
408
- */
409
- export const psqlErrorPrefix = (settings, lineNumber) => {
410
- if (settings.curCmdSource === "file" && settings.inputfile) {
411
- const lineSuffix = lineNumber !== undefined ? String(lineNumber) : "";
412
- return `psql:${settings.inputfile}:${lineSuffix}: `;
413
- }
414
- return "";
342
+ * Build the `psql:` diagnostic prefix that upstream `pg_log_pre_callback`
343
+ * prepends to error lines, but ONLY when reading from a script file. Mirrors:
344
+ *
345
+ * if (cur_cmd_source == QUERY_FROM_FILE)
346
+ * fprintf(stderr, "psql:%s:%d: ", cur_cmd_filename, cur_cmd_lineno);
347
+ *
348
+ * - `curCmdSource === 'file'` (running under `-f FILE`, `\i FILE`,
349
+ * `\ir FILE`, or `.psqlrc`): `psql:<inputfile>:<lineno>: `.
350
+ * - Stdin pipe, `-c "..."`, interactive REPL: empty string — vanilla
351
+ * `psql --no-psqlrc -X` reading SQL from stdin emits NO prefix on
352
+ * either `\set` validation errors or server `ERROR:` lines.
353
+ *
354
+ * Returned string ends in a trailing space when non-empty so callers can
355
+ * concatenate the severity directly (`prefix + 'ERROR: msg'`).
356
+ */
357
+ const psqlErrorPrefix = (settings, lineNumber) => {
358
+ if (settings.curCmdSource === "file" && settings.inputfile) {
359
+ const lineSuffix = lineNumber !== void 0 ? String(lineNumber) : "";
360
+ return `psql:${settings.inputfile}:${lineSuffix}: `;
361
+ }
362
+ return "";
415
363
  };
416
364
  /**
417
- * Walk past leading whitespace + `--` line comments + slash-star block
418
- * comments at the head of `sqlText`. Returns the byte index of the first
419
- * "real" content character. Used by `renderLineAndCaret` to align the
420
- * `LINE N:` counter with upstream psql — vanilla strips these from the
421
- * buffer before `PQexec` (so the server's `position` is relative to the
422
- * trimmed buffer), but `captureLastError`/`normaliseSqlAndPosition` only
423
- * strips whitespace. Re-stripping here closes the gap when the captured
424
- * `sqlText` still carries leading `-- comment` lines (the common case
425
- * for SQL that the mainloop dispatched directly via `sendQuery`,
426
- * because that path doesn't pre-trim comments).
427
- *
428
- * Idempotent for already-trimmed input: if `sqlText` has no leading
429
- * prelude we return `0`, the caller takes the existing fast-path, and
430
- * the LINE count remains the count of newlines strictly before
431
- * `position - 1`.
432
- */
365
+ * Walk past leading whitespace + `--` line comments + slash-star block
366
+ * comments at the head of `sqlText`. Returns the byte index of the first
367
+ * "real" content character. Used by `renderLineAndCaret` to align the
368
+ * `LINE N:` counter with upstream psql — vanilla strips these from the
369
+ * buffer before `PQexec` (so the server's `position` is relative to the
370
+ * trimmed buffer), but `captureLastError`/`normaliseSqlAndPosition` only
371
+ * strips whitespace. Re-stripping here closes the gap when the captured
372
+ * `sqlText` still carries leading `-- comment` lines (the common case
373
+ * for SQL that the mainloop dispatched directly via `sendQuery`,
374
+ * because that path doesn't pre-trim comments).
375
+ *
376
+ * Idempotent for already-trimmed input: if `sqlText` has no leading
377
+ * prelude we return `0`, the caller takes the existing fast-path, and
378
+ * the LINE count remains the count of newlines strictly before
379
+ * `position - 1`.
380
+ */
433
381
  const skipLeadingPrelude = (sqlText) => {
434
- let i = 0;
435
- const n = sqlText.length;
436
- while (i < n) {
437
- const c = sqlText.charCodeAt(i);
438
- if (c === 0x20 ||
439
- c === 0x09 ||
440
- c === 0x0a ||
441
- c === 0x0d ||
442
- c === 0x0c ||
443
- c === 0x0b) {
444
- i++;
445
- continue;
446
- }
447
- if (c === 0x2d && sqlText.charCodeAt(i + 1) === 0x2d) {
448
- i += 2;
449
- while (i < n && sqlText.charCodeAt(i) !== 0x0a)
450
- i++;
451
- continue;
452
- }
453
- if (c === 0x2f && sqlText.charCodeAt(i + 1) === 0x2a) {
454
- i += 2;
455
- let depth = 1;
456
- while (i < n && depth > 0) {
457
- if (sqlText.charCodeAt(i) === 0x2f &&
458
- sqlText.charCodeAt(i + 1) === 0x2a) {
459
- depth++;
460
- i += 2;
461
- }
462
- else if (sqlText.charCodeAt(i) === 0x2a &&
463
- sqlText.charCodeAt(i + 1) === 0x2f) {
464
- depth--;
465
- i += 2;
466
- }
467
- else {
468
- i++;
469
- }
470
- }
471
- continue;
472
- }
473
- break;
474
- }
475
- return i;
382
+ let i = 0;
383
+ const n = sqlText.length;
384
+ while (i < n) {
385
+ const c = sqlText.charCodeAt(i);
386
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12 || c === 11) {
387
+ i++;
388
+ continue;
389
+ }
390
+ if (c === 45 && sqlText.charCodeAt(i + 1) === 45) {
391
+ i += 2;
392
+ while (i < n && sqlText.charCodeAt(i) !== 10) i++;
393
+ continue;
394
+ }
395
+ if (c === 47 && sqlText.charCodeAt(i + 1) === 42) {
396
+ i += 2;
397
+ let depth = 1;
398
+ while (i < n && depth > 0) if (sqlText.charCodeAt(i) === 47 && sqlText.charCodeAt(i + 1) === 42) {
399
+ depth++;
400
+ i += 2;
401
+ } else if (sqlText.charCodeAt(i) === 42 && sqlText.charCodeAt(i + 1) === 47) {
402
+ depth--;
403
+ i += 2;
404
+ } else i++;
405
+ continue;
406
+ }
407
+ break;
408
+ }
409
+ return i;
476
410
  };
477
411
  /**
478
- * Render the `LINE N: …` re-print plus the `^` pointer underneath the
479
- * failing character, mirroring upstream psql's `report_error_query`
480
- * helper. Returns `null` when we don't have enough context (no SQL text
481
- * or no position) so the caller can skip the lines entirely.
482
- *
483
- * `position` is a 1-based character offset into `sqlText` (as delivered
484
- * in the server's `P` field). We pick the LINE containing that offset
485
- * and emit:
486
- *
487
- * LINE N: <that line>
488
- * ^
489
- *
490
- * The caret column is aligned to the offset within the picked line so
491
- * it points at the failing token. Trailing newlines on the picked line
492
- * are stripped so the `$` end-anchor in upstream's regex still matches.
493
- *
494
- * Leading whitespace + comments are skipped before computing the LINE
495
- * number so the count starts at the first content line — vanilla
496
- * advances past these before `PQexec`, so the server's `position` is
497
- * 1-based relative to a trimmed buffer; without the same skip here we'd
498
- * count newlines that vanilla never sent.
499
- *
500
- * Trailing-whitespace fix-up: callers in `cmd_io.ts` strip a `\g`-style
501
- * buffer's trailing whitespace before handing the SQL to `db.execSimple`
502
- * / `db.query`, so the server's `position` is relative to the trimmed
503
- * SQL while `sqlText` (used for the LINE echo) still carries the
504
- * trailing space(s). When `position` lands on trailing whitespace of
505
- * the LINE we picked — i.e., past `lineText.trimEnd().length` — that's
506
- * the "syntax error at end of input" case: vanilla sends the trailing
507
- * whitespace verbatim and the server reports a position one past the
508
- * full LINE length. Snap the caret to the end of `lineText` so our
509
- * output matches vanilla's `^` column. PostgreSQL's scanner never
510
- * emits positions pointing AT whitespace tokens (they're not lexed as
511
- * tokens), so the only realistic source of an "in trailing
512
- * whitespace" position is this trim-on-send delta.
513
- *
514
- * Exported so the per-statement error renderer in `core/common.ts` can
515
- * share the helper with `\errverbose`.
516
- */
517
- export const renderLineAndCaret = (sqlText, position) => {
518
- if (!sqlText || !position)
519
- return null;
520
- const pos = parseInt(position, 10);
521
- if (!Number.isFinite(pos) || pos <= 0)
522
- return null;
523
- // Advance past leading WS + comments so the LINE count starts at the
524
- // first content line. The server's position is into the on-the-wire
525
- // bytes — typically already past these, so rebasing keeps it inside
526
- // the content range; if the rebased position would underflow we drop
527
- // the LINE/caret block rather than mis-pointing.
528
- const skip = skipLeadingPrelude(sqlText);
529
- const trimmed = skip === 0 ? sqlText : sqlText.slice(skip);
530
- const rebasedPos = pos - skip;
531
- if (rebasedPos <= 0)
532
- return null;
533
- // The server's offset is 1-based and points at the failing character.
534
- const idx = Math.min(rebasedPos - 1, trimmed.length);
535
- // Find the line containing `idx`.
536
- let lineStart = trimmed.lastIndexOf("\n", idx - 1);
537
- lineStart = lineStart === -1 ? 0 : lineStart + 1;
538
- let lineEnd = trimmed.indexOf("\n", lineStart);
539
- if (lineEnd === -1)
540
- lineEnd = trimmed.length;
541
- const lineText = trimmed.slice(lineStart, lineEnd);
542
- // Line number for the `LINE N:` prefix — 1-based.
543
- const before = trimmed.slice(0, lineStart);
544
- const lineNumber = (before.match(/\n/gu)?.length ?? 0) + 1;
545
- // Column inside the picked line (0-based) where the `^` goes. Tabs
546
- // upstream are expanded to a fixed width; we approximate with a
547
- // single space so the pointer at least lands in the right ballpark.
548
- let col = idx - lineStart;
549
- // Snap past trailing whitespace when the position lands inside it —
550
- // see the function header for the rationale (trim-on-send delta).
551
- const lineTrimEndLen = lineText.replace(/[ \t\f\v]+$/u, "").length;
552
- if (col >= lineTrimEndLen && col < lineText.length) {
553
- col = lineText.length;
554
- }
555
- const caretIndent = " ".repeat(Math.max(0, col));
556
- const prefix = `LINE ${String(lineNumber)}: `;
557
- return {
558
- line: `${prefix}${lineText}`,
559
- caret: `${" ".repeat(prefix.length)}${caretIndent}^`,
560
- };
412
+ * Render the `LINE N: …` re-print plus the `^` pointer underneath the
413
+ * failing character, mirroring upstream psql's `report_error_query`
414
+ * helper. Returns `null` when we don't have enough context (no SQL text
415
+ * or no position) so the caller can skip the lines entirely.
416
+ *
417
+ * `position` is a 1-based character offset into `sqlText` (as delivered
418
+ * in the server's `P` field). We pick the LINE containing that offset
419
+ * and emit:
420
+ *
421
+ * LINE N: <that line>
422
+ * ^
423
+ *
424
+ * The caret column is aligned to the offset within the picked line so
425
+ * it points at the failing token. Trailing newlines on the picked line
426
+ * are stripped so the `$` end-anchor in upstream's regex still matches.
427
+ *
428
+ * Leading whitespace + comments are skipped before computing the LINE
429
+ * number so the count starts at the first content line — vanilla
430
+ * advances past these before `PQexec`, so the server's `position` is
431
+ * 1-based relative to a trimmed buffer; without the same skip here we'd
432
+ * count newlines that vanilla never sent.
433
+ *
434
+ * Trailing-whitespace fix-up: callers in `cmd_io.ts` strip a `\g`-style
435
+ * buffer's trailing whitespace before handing the SQL to `db.execSimple`
436
+ * / `db.query`, so the server's `position` is relative to the trimmed
437
+ * SQL while `sqlText` (used for the LINE echo) still carries the
438
+ * trailing space(s). When `position` lands on trailing whitespace of
439
+ * the LINE we picked — i.e., past `lineText.trimEnd().length` — that's
440
+ * the "syntax error at end of input" case: vanilla sends the trailing
441
+ * whitespace verbatim and the server reports a position one past the
442
+ * full LINE length. Snap the caret to the end of `lineText` so our
443
+ * output matches vanilla's `^` column. PostgreSQL's scanner never
444
+ * emits positions pointing AT whitespace tokens (they're not lexed as
445
+ * tokens), so the only realistic source of an "in trailing
446
+ * whitespace" position is this trim-on-send delta.
447
+ *
448
+ * Exported so the per-statement error renderer in `core/common.ts` can
449
+ * share the helper with `\errverbose`.
450
+ */
451
+ const renderLineAndCaret = (sqlText, position) => {
452
+ if (!sqlText || !position) return null;
453
+ const pos = parseInt(position, 10);
454
+ if (!Number.isFinite(pos) || pos <= 0) return null;
455
+ const skip = skipLeadingPrelude(sqlText);
456
+ const trimmed = skip === 0 ? sqlText : sqlText.slice(skip);
457
+ const rebasedPos = pos - skip;
458
+ if (rebasedPos <= 0) return null;
459
+ const idx = Math.min(rebasedPos - 1, trimmed.length);
460
+ let lineStart = trimmed.lastIndexOf("\n", idx - 1);
461
+ lineStart = lineStart === -1 ? 0 : lineStart + 1;
462
+ let lineEnd = trimmed.indexOf("\n", lineStart);
463
+ if (lineEnd === -1) lineEnd = trimmed.length;
464
+ const lineText = trimmed.slice(lineStart, lineEnd);
465
+ const lineNumber = (trimmed.slice(0, lineStart).match(/\n/gu)?.length ?? 0) + 1;
466
+ let col = idx - lineStart;
467
+ const lineTrimEndLen = lineText.replace(/[ \t\f\v]+$/u, "").length;
468
+ if (col >= lineTrimEndLen && col < lineText.length) col = lineText.length;
469
+ const caretIndent = " ".repeat(Math.max(0, col));
470
+ const prefix = `LINE ${String(lineNumber)}: `;
471
+ return {
472
+ line: `${prefix}${lineText}`,
473
+ caret: `${" ".repeat(prefix.length)}${caretIndent}^`
474
+ };
561
475
  };
562
476
  /**
563
- * Render an ErrorResponse-shaped payload as the layered, verbosity-aware
564
- * report that upstream psql emits to stderr after a failed statement
565
- * (`PSQLExec` / `ProcessResult` in `src/bin/psql/common.c`).
566
- *
567
- * Returned array contains one element per logical line, without trailing
568
- * newlines — callers join with `\n` and write to their stream.
569
- *
570
- * Verbosity / SHOW_CONTEXT semantics, mirrored from upstream:
571
- *
572
- * - `terse`: only the severity line (`<sev>: <msg>`) is emitted.
573
- *
574
- * - `default`: severity + message, plus `LINE N` / caret, DETAIL, HINT,
575
- * STATEMENT (we omit STATEMENT — we never echo the query verbatim).
576
- * CONTEXT and LOCATION are suppressed unless `SHOW_CONTEXT='always'`.
577
- *
578
- * - `verbose`: adds the SQLSTATE prefix on the severity line, and
579
- * CONTEXT plus LOCATION are unconditionally included when present.
580
- *
581
- * - `sqlstate`: prepend the SQLSTATE on the severity line (same as
582
- * `verbose`'s first line), but suppress LINE/DETAIL/HINT/CONTEXT/
583
- * LOCATION. Matches the upstream "just give me the code" flavour.
584
- *
585
- * Empty server fields are skipped silently. The `LINE` / `^` pair only
586
- * appears when we have both originating SQL text and a 1-based position
587
- * pointing inside it.
588
- */
589
- export const formatErrorReport = (e, verbosity = "default", showContext = "errors") => {
590
- const severity = e.severity ?? "ERROR";
591
- const sqlstate = e.code ?? e.sqlstate ?? "XX000";
592
- const message = e.message ?? "";
593
- const out = [];
594
- // `sqlstate` mode is the upstream "just give me the code" flavour:
595
- // emit `<severity>: <sqlstate>` with NO message body. `verbose` mode
596
- // adds the SQLSTATE prefix and keeps the message + LINE/DETAIL/HINT
597
- // layers below. Default/terse omit the SQLSTATE entirely.
598
- //
599
- // Reference: upstream `pg_log_pre_callback` / `PQresultErrorMessage`
600
- // with `verbosity = PQERRORS_SQLSTATE`, which formats just
601
- // `severity: sqlstate\n` and stops.
602
- if (verbosity === "sqlstate") {
603
- out.push(`${severity}: ${sqlstate}`);
604
- return out;
605
- }
606
- if (verbosity === "verbose") {
607
- out.push(`${severity}: ${sqlstate}: ${message}`);
608
- }
609
- else if (verbosity === "terse") {
610
- // Terse suppresses LINE/caret/DETAIL/HINT/CONTEXT, but it merges the
611
- // server's `position` into the severity line as `at character N` —
612
- // matches libpq's `pqGetErrorNotice3` with `PQERRORS_TERSE` (and
613
- // vanilla psql in the regress fixture). Only fires when position is a
614
- // positive integer; the LINE/caret block below would have shown the
615
- // same anchor for default verbosity.
616
- const pos = e.position ? Number.parseInt(e.position, 10) : NaN;
617
- if (Number.isFinite(pos) && pos > 0) {
618
- out.push(`${severity}: ${message} at character ${String(pos)}`);
619
- }
620
- else {
621
- out.push(`${severity}: ${message}`);
622
- }
623
- return out;
624
- }
625
- else {
626
- out.push(`${severity}: ${message}`);
627
- }
628
- const lineCaret = renderLineAndCaret(e.sqlText, e.position);
629
- if (lineCaret) {
630
- out.push(lineCaret.line);
631
- out.push(lineCaret.caret);
632
- }
633
- if (e.detail)
634
- out.push(`DETAIL: ${e.detail}`);
635
- if (e.hint)
636
- out.push(`HINT: ${e.hint}`);
637
- // CONTEXT under default verbosity follows SHOW_CONTEXT: 'never' / 'errors'
638
- // (the default — show on errors) / 'always'. We treat every call into the
639
- // formatter as an error report, so 'errors' and 'always' both include
640
- // CONTEXT, while 'never' suppresses it. Verbose verbosity unconditionally
641
- // includes CONTEXT.
642
- const includeContext = verbosity === "verbose" || showContext !== "never";
643
- if (includeContext && e.where) {
644
- out.push(`CONTEXT: ${e.where}`);
645
- }
646
- if (verbosity === "verbose" && (e.routine || e.file || e.line)) {
647
- const location = (e.routine ?? "") + (e.file ? `, ${e.file}:${e.line ?? ""}` : "");
648
- out.push(`LOCATION: ${location}`);
649
- }
650
- return out;
477
+ * Render an ErrorResponse-shaped payload as the layered, verbosity-aware
478
+ * report that upstream psql emits to stderr after a failed statement
479
+ * (`PSQLExec` / `ProcessResult` in `src/bin/psql/common.c`).
480
+ *
481
+ * Returned array contains one element per logical line, without trailing
482
+ * newlines — callers join with `\n` and write to their stream.
483
+ *
484
+ * Verbosity / SHOW_CONTEXT semantics, mirrored from upstream:
485
+ *
486
+ * - `terse`: only the severity line (`<sev>: <msg>`) is emitted.
487
+ *
488
+ * - `default`: severity + message, plus `LINE N` / caret, DETAIL, HINT,
489
+ * STATEMENT (we omit STATEMENT — we never echo the query verbatim).
490
+ * CONTEXT and LOCATION are suppressed unless `SHOW_CONTEXT='always'`.
491
+ *
492
+ * - `verbose`: adds the SQLSTATE prefix on the severity line, and
493
+ * CONTEXT plus LOCATION are unconditionally included when present.
494
+ *
495
+ * - `sqlstate`: prepend the SQLSTATE on the severity line (same as
496
+ * `verbose`'s first line), but suppress LINE/DETAIL/HINT/CONTEXT/
497
+ * LOCATION. Matches the upstream "just give me the code" flavour.
498
+ *
499
+ * Empty server fields are skipped silently. The `LINE` / `^` pair only
500
+ * appears when we have both originating SQL text and a 1-based position
501
+ * pointing inside it.
502
+ */
503
+ const formatErrorReport = (e, verbosity = "default", showContext = "errors") => {
504
+ const severity = e.severity ?? "ERROR";
505
+ const sqlstate = e.code ?? e.sqlstate ?? "XX000";
506
+ const message = e.message ?? "";
507
+ const out = [];
508
+ if (verbosity === "sqlstate") {
509
+ out.push(`${severity}: ${sqlstate}`);
510
+ return out;
511
+ }
512
+ if (verbosity === "verbose") out.push(`${severity}: ${sqlstate}: ${message}`);
513
+ else if (verbosity === "terse") {
514
+ const pos = e.position ? Number.parseInt(e.position, 10) : NaN;
515
+ if (Number.isFinite(pos) && pos > 0) out.push(`${severity}: ${message} at character ${String(pos)}`);
516
+ else out.push(`${severity}: ${message}`);
517
+ return out;
518
+ } else out.push(`${severity}: ${message}`);
519
+ const lineCaret = renderLineAndCaret(e.sqlText, e.position);
520
+ if (lineCaret) {
521
+ out.push(lineCaret.line);
522
+ out.push(lineCaret.caret);
523
+ }
524
+ if (e.detail) out.push(`DETAIL: ${e.detail}`);
525
+ if (e.hint) out.push(`HINT: ${e.hint}`);
526
+ if ((verbosity === "verbose" || showContext !== "never") && e.where) out.push(`CONTEXT: ${e.where}`);
527
+ if (verbosity === "verbose" && (e.routine || e.file || e.line)) {
528
+ const location = (e.routine ?? "") + (e.file ? `, ${e.file}:${e.line ?? ""}` : "");
529
+ out.push(`LOCATION: ${location}`);
530
+ }
531
+ return out;
651
532
  };
652
533
  /**
653
- * `\errverbose` — print the last error in verbose form. We rely on the
654
- * mainloop to have stored `settings.lastErrorResult`; this command only
655
- * formats and prints. Without a saved error, upstream emits "There is no
656
- * previous error."
657
- *
658
- * Verbose output (PG 18 form):
659
- *
660
- * ERROR: <sqlstate>: <message>
661
- * LINE N: <originating line of SQL>
662
- * ^
663
- * DETAIL: <detail>
664
- * HINT: <hint>
665
- * CONTEXT: <where>
666
- * LOCATION: <routine>, <file>:<line>
667
- *
668
- * Empty fields are omitted. The `LINE` / `^` pair is only emitted when
669
- * we have both the originating SQL text and a server-provided position.
670
- */
671
- export const cmdErrverbose = {
672
- name: "errverbose",
673
- helpKey: "errverbose",
674
- run: (ctx) => {
675
- const e = ctx.settings.lastErrorResult;
676
- if (!e || (!e.message && !e.sqlstate && !e.code)) {
677
- // Upstream `exec_command_errverbose` writes the "no previous error"
678
- // notice to stdout (via `printf`); only the verbose re-render goes to
679
- // stderr (via `pg_log_error`).
680
- writeOut("There is no previous error.\n");
681
- return Promise.resolve({ status: "ok" });
682
- }
683
- // `\errverbose` always emits the full verbose form regardless of the
684
- // currently active VERBOSITY setting. Output is prefixed with the same
685
- // `psql:[<file>:<n>]:` tag upstream's `pg_log_pre_callback` adds — only
686
- // on the leading severity line; subsequent layers (LINE / caret / DETAIL
687
- // / HINT / LOCATION) stay unprefixed to match libpq's `PQresultErrorMessage`.
688
- const lines = formatErrorReport(e, "verbose", "always");
689
- const prefix = psqlErrorPrefix(ctx.settings);
690
- const prefixed = [prefix + lines[0], ...lines.slice(1)];
691
- writeErr(prefixed.join("\n") + "\n");
692
- return Promise.resolve({ status: "ok" });
693
- },
534
+ * `\errverbose` — print the last error in verbose form. We rely on the
535
+ * mainloop to have stored `settings.lastErrorResult`; this command only
536
+ * formats and prints. Without a saved error, upstream emits "There is no
537
+ * previous error."
538
+ *
539
+ * Verbose output (PG 18 form):
540
+ *
541
+ * ERROR: <sqlstate>: <message>
542
+ * LINE N: <originating line of SQL>
543
+ * ^
544
+ * DETAIL: <detail>
545
+ * HINT: <hint>
546
+ * CONTEXT: <where>
547
+ * LOCATION: <routine>, <file>:<line>
548
+ *
549
+ * Empty fields are omitted. The `LINE` / `^` pair is only emitted when
550
+ * we have both the originating SQL text and a server-provided position.
551
+ */
552
+ const cmdErrverbose = {
553
+ name: "errverbose",
554
+ helpKey: "errverbose",
555
+ run: (ctx) => {
556
+ const e = ctx.settings.lastErrorResult;
557
+ if (!e || !e.message && !e.sqlstate && !e.code) {
558
+ writeOut("There is no previous error.\n");
559
+ return Promise.resolve({ status: "ok" });
560
+ }
561
+ const lines = formatErrorReport(e, "verbose", "always");
562
+ const prefixed = [psqlErrorPrefix(ctx.settings) + lines[0], ...lines.slice(1)];
563
+ writeErr(prefixed.join("\n") + "\n");
564
+ return Promise.resolve({ status: "ok" });
565
+ }
694
566
  };
695
567
  /**
696
- * `\timing [on|off]` — set `settings.timing`. With no arg the value is
697
- * flipped. Prints the new state to stdout. `toggle` is NOT a valid value —
698
- * upstream errors "Boolean expected" (review: minor divergences).
699
- */
700
- export const cmdTiming = {
701
- name: "timing",
702
- helpKey: "timing",
703
- run: (ctx) => {
704
- const arg = ctx.nextArg("normal");
705
- let next;
706
- if (arg === null) {
707
- next = !ctx.settings.timing;
708
- }
709
- else {
710
- const parsed = parseBool(arg);
711
- if (parsed === null) {
712
- writeErr(`\\${ctx.cmdName}: unrecognized value "${arg}" for "\\timing": Boolean expected\n`);
713
- return Promise.resolve({ status: "error" });
714
- }
715
- next = parsed;
716
- }
717
- ctx.settings.timing = next;
718
- writeOut(`Timing is ${next ? "on" : "off"}.\n`);
719
- return Promise.resolve({ status: "ok" });
720
- },
568
+ * `\timing [on|off]` — set `settings.timing`. With no arg the value is
569
+ * flipped. Prints the new state to stdout. `toggle` is NOT a valid value —
570
+ * upstream errors "Boolean expected" (review: minor divergences).
571
+ */
572
+ const cmdTiming = {
573
+ name: "timing",
574
+ helpKey: "timing",
575
+ run: (ctx) => {
576
+ const arg = ctx.nextArg("normal");
577
+ let next;
578
+ if (arg === null) next = !ctx.settings.timing;
579
+ else {
580
+ const parsed = parseBool(arg);
581
+ if (parsed === null) {
582
+ writeErr(`\\${ctx.cmdName}: unrecognized value "${arg}" for "\\timing": Boolean expected\n`);
583
+ return Promise.resolve({ status: "error" });
584
+ }
585
+ next = parsed;
586
+ }
587
+ ctx.settings.timing = next;
588
+ writeOut(`Timing is ${next ? "on" : "off"}.\n`);
589
+ return Promise.resolve({ status: "ok" });
590
+ }
721
591
  };
722
592
  /**
723
- * Static text emitted by `\copyright`. Mirrors upstream psql's
724
- * `exec_command_copyright()` literal in `src/bin/psql/command.c`.
725
- */
593
+ * Static text emitted by `\copyright`. Mirrors upstream psql's
594
+ * `exec_command_copyright()` literal in `src/bin/psql/command.c`.
595
+ */
726
596
  const COPYRIGHT_TEXT = `PostgreSQL Database Management System
727
597
  (formerly known as Postgres, then as Postgres95)
728
598
 
@@ -748,11 +618,11 @@ ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
748
618
  PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
749
619
  `;
750
620
  /**
751
- * neonctl-specific notice appended after the upstream PostgreSQL block.
752
- * This psql is a pure-TypeScript reimplementation embedded in neonctl, not
753
- * upstream psql — so we attribute it accordingly. No license claim is made
754
- * here on purpose (see the project's LICENSE file for terms).
755
- */
621
+ * neonctl-specific notice appended after the upstream PostgreSQL block.
622
+ * This psql is a pure-TypeScript reimplementation embedded in neonctl, not
623
+ * upstream psql — so we attribute it accordingly. No license claim is made
624
+ * here on purpose (see the project's LICENSE file for terms).
625
+ */
756
626
  const NEON_NOTICE = `
757
627
  This is an embedded psql reimplementation that ships with neon, the
758
628
  command-line interface for Neon (https://neon.tech). Neon is part of
@@ -763,207 +633,186 @@ or endorsed by the PostgreSQL Global Development Group. See the neon
763
633
  LICENSE file for distribution terms.
764
634
  `;
765
635
  /**
766
- * `\copyright` — print the PostgreSQL copyright / license notice, followed
767
- * by a neonctl + Neon/Databricks attribution block. Takes no arguments.
768
- * The upstream block is preserved verbatim so the conformance regex
769
- * `/Copyright/` (from upstream `001_basic.pl` line 75) is satisfied; the
770
- * Neon notice is appended after it.
771
- */
772
- export const cmdCopyright = {
773
- name: "copyright",
774
- helpKey: "copyright",
775
- run: () => {
776
- writeOut(COPYRIGHT_TEXT + NEON_NOTICE);
777
- return Promise.resolve({ status: "ok" });
778
- },
636
+ * `\copyright` — print the PostgreSQL copyright / license notice, followed
637
+ * by a neonctl + Neon/Databricks attribution block. Takes no arguments.
638
+ * The upstream block is preserved verbatim so the conformance regex
639
+ * `/Copyright/` (from upstream `001_basic.pl` line 75) is satisfied; the
640
+ * Neon notice is appended after it.
641
+ */
642
+ const cmdCopyright = {
643
+ name: "copyright",
644
+ helpKey: "copyright",
645
+ run: () => {
646
+ writeOut(COPYRIGHT_TEXT + NEON_NOTICE);
647
+ return Promise.resolve({ status: "ok" });
648
+ }
779
649
  };
780
650
  /**
781
- * Terminal width used to lay out `\h` / `\help` topic lists. Upstream
782
- * uses `pset.popt.topt.envColumns` falling back to `ioctl(TIOCGWINSZ)`;
783
- * we read `process.stdout.columns` (Node populates this for TTYs) and
784
- * default to 80 if absent (non-TTY, piped output, etc.).
785
- */
651
+ * Terminal width used to lay out `\h` / `\help` topic lists. Upstream
652
+ * uses `pset.popt.topt.envColumns` falling back to `ioctl(TIOCGWINSZ)`;
653
+ * we read `process.stdout.columns` (Node populates this for TTYs) and
654
+ * default to 80 if absent (non-TTY, piped output, etc.).
655
+ */
786
656
  const screenWidth = () => {
787
- const cols = process.stdout.columns;
788
- return typeof cols === "number" && cols > 0 ? cols : 80;
657
+ const cols = process.stdout.columns;
658
+ return typeof cols === "number" && cols > 0 ? cols : 80;
789
659
  };
790
660
  /**
791
- * `\h [TOPIC]` (alias `\help`) — show SQL command help.
792
- *
793
- * Delegates to {@link helpSQL} in `core/help.ts`, passing the remainder
794
- * of the line as the topic. With no topic, prints the "Available help:"
795
- * overview; with a topic, prints the matching synopsis or a list of
796
- * matches. Mirrors upstream `exec_command_help` in `command.c`.
797
- */
798
- export const cmdHelpSQL = {
799
- name: "h",
800
- aliases: ["help"],
801
- helpKey: "h",
802
- run: (ctx) => {
803
- // Upstream consumes the rest of the line in `OT_WHOLE_LINE` mode so
804
- // multi-word topics like "CREATE TABLE" come through intact.
805
- const topic = ctx.restOfLine();
806
- helpSQL(process.stdout, topic.length === 0 ? null : topic, screenWidth());
807
- return Promise.resolve({ status: "ok" });
808
- },
661
+ * `\h [TOPIC]` (alias `\help`) — show SQL command help.
662
+ *
663
+ * Delegates to {@link helpSQL} in `core/help.ts`, passing the remainder
664
+ * of the line as the topic. With no topic, prints the "Available help:"
665
+ * overview; with a topic, prints the matching synopsis or a list of
666
+ * matches. Mirrors upstream `exec_command_help` in `command.c`.
667
+ */
668
+ const cmdHelpSQL = {
669
+ name: "h",
670
+ aliases: ["help"],
671
+ helpKey: "h",
672
+ run: (ctx) => {
673
+ const topic = ctx.restOfLine();
674
+ helpSQL(process.stdout, topic.length === 0 ? null : topic, screenWidth());
675
+ return Promise.resolve({ status: "ok" });
676
+ }
809
677
  };
810
678
  /**
811
- * `\?` — show help for the backslash commands.
812
- *
813
- * Delegates to {@link slashUsage} in `core/help.ts`. We pass the output
814
- * stream (`process.stdout`) and request the pager only when that stream is
815
- * an interactive TTY — `slashUsage`/`emitHelp` re-check interactivity, but
816
- * gating the request here keeps the non-interactive path (scripts, piped
817
- * output, the regress harness) writing straight to stdout with no pager.
818
- *
819
- * Upstream `exec_command_help` reads `[commands|options|variables]`; we
820
- * mirror the default (backslash commands) form, which is the only variant
821
- * `\?` reaches without an argument. The remainder of the line is consumed
822
- * so a stray topic doesn't leak into the next command.
823
- */
824
- export const cmdSlashHelp = {
825
- name: "?",
826
- argMode: "whole-line",
827
- helpKey: "?",
828
- run: (ctx) => {
829
- // Consume the rest of the line (`\? options`, `\? variables`) so the
830
- // cursor doesn't strand trailing text; we only render the command help.
831
- ctx.restOfLine();
832
- const out = process.stdout;
833
- const pager = Boolean(out.isTTY);
834
- slashUsage(out, pager);
835
- return Promise.resolve({ status: "ok" });
836
- },
679
+ * `\?` — show help for the backslash commands.
680
+ *
681
+ * Delegates to {@link slashUsage} in `core/help.ts`. We pass the output
682
+ * stream (`process.stdout`) and request the pager only when that stream is
683
+ * an interactive TTY — `slashUsage`/`emitHelp` re-check interactivity, but
684
+ * gating the request here keeps the non-interactive path (scripts, piped
685
+ * output, the regress harness) writing straight to stdout with no pager.
686
+ *
687
+ * Upstream `exec_command_help` reads `[commands|options|variables]`; we
688
+ * mirror the default (backslash commands) form, which is the only variant
689
+ * `\?` reaches without an argument. The remainder of the line is consumed
690
+ * so a stray topic doesn't leak into the next command.
691
+ */
692
+ const cmdSlashHelp = {
693
+ name: "?",
694
+ argMode: "whole-line",
695
+ helpKey: "?",
696
+ run: (ctx) => {
697
+ ctx.restOfLine();
698
+ const out = process.stdout;
699
+ const pager = Boolean(out.isTTY);
700
+ slashUsage(out, pager);
701
+ return Promise.resolve({ status: "ok" });
702
+ }
837
703
  };
838
704
  /**
839
- * Resolve the editor command psql would launch for `\e` / `\ef` / `\ev`,
840
- * mirroring upstream `editFile` / `get_alternate_expansion`:
841
- *
842
- * $PSQL_EDITOR || $EDITOR || $VISUAL || platform default
843
- *
844
- * The platform default is `notepad.exe` on Windows and `vi` elsewhere,
845
- * matching upstream's `DEFAULT_EDITOR`.
846
- */
847
- export const resolveEditor = (env = process.env) => {
848
- const explicit = env.PSQL_EDITOR ?? env.EDITOR ?? env.VISUAL;
849
- if (explicit !== undefined && explicit.length > 0)
850
- return explicit;
851
- return process.platform === "win32" ? "notepad.exe" : "vi";
705
+ * Resolve the editor command psql would launch for `\e` / `\ef` / `\ev`,
706
+ * mirroring upstream `editFile` / `get_alternate_expansion`:
707
+ *
708
+ * $PSQL_EDITOR || $EDITOR || $VISUAL || platform default
709
+ *
710
+ * The platform default is `notepad.exe` on Windows and `vi` elsewhere,
711
+ * matching upstream's `DEFAULT_EDITOR`.
712
+ */
713
+ const resolveEditor = (env = process.env) => {
714
+ const explicit = env.PSQL_EDITOR ?? env.EDITOR ?? env.VISUAL;
715
+ if (explicit !== void 0 && explicit.length > 0) return explicit;
716
+ return process.platform === "win32" ? "notepad.exe" : "vi";
852
717
  };
853
718
  /**
854
- * `\e` / `\edit [FILE] [LINE]` — edit the current query buffer (or a file)
855
- * in the user's editor, then load the edited text back into the query
856
- * buffer.
857
- *
858
- * This port implements the common no-FILE form: dump the current query
859
- * buffer to a temp file, spawn the editor on it inheriting stdio (upstream
860
- * `do_edit` → `editFile`), and on a clean exit read the file back and
861
- * return it as the new query buffer via `status: 'reset-buf'`. Upstream
862
- * strips a single trailing newline the editor may add; we do the same so
863
- * round-tripping an unchanged buffer is a no-op.
864
- *
865
- * Editor selection follows {@link resolveEditor}. The spawn uses
866
- * `spawnSync(..., { stdio: 'inherit' })` so the editor owns the terminal.
867
- * If the editor exits non-zero (or fails to spawn) we leave the buffer
868
- * untouched and report an error, matching upstream's behaviour of not
869
- * importing a failed edit.
870
- *
871
- * FILE / LINE arguments are accepted but the buffer is still seeded from
872
- * the current query buffer; a future WP can layer file-backed editing
873
- * (`\e file`) and `\ef`/`\ev` on top.
874
- */
875
- export const cmdEdit = {
876
- name: "e",
877
- aliases: ["edit"],
878
- argMode: "whole-line",
879
- helpKey: "e",
880
- run: (ctx) => {
881
- // We don't yet support `\e FILE`; consume the args so they don't strand.
882
- ctx.restOfLine();
883
- const editor = resolveEditor();
884
- // psql seeds the temp file with the current query buffer. A trailing
885
- // newline keeps editors that expect newline-terminated files happy.
886
- const seed = ctx.queryBuf.length > 0 && !ctx.queryBuf.endsWith("\n")
887
- ? ctx.queryBuf + "\n"
888
- : ctx.queryBuf;
889
- let dir = null;
890
- try {
891
- dir = mkdtempSync(join(tmpdir(), "psql.edit."));
892
- const file = join(dir, "edit.sql");
893
- writeFileSync(file, seed, "utf8");
894
- const result = spawnSync(editor, [file], { stdio: "inherit" });
895
- if (result.error ||
896
- (result.status !== null && result.status !== 0)) {
897
- const why = result.error
898
- ? result.error.message
899
- : `editor exited with status ${String(result.status)}`;
900
- writeErr(`\\${ctx.cmdName}: ${why}\n`);
901
- return Promise.resolve({ status: "error" });
902
- }
903
- let edited = readFileSync(file, "utf8");
904
- // Upstream drops a single trailing newline the editor may have added
905
- // so an unchanged round-trip restores the original buffer exactly.
906
- if (edited.endsWith("\n"))
907
- edited = edited.slice(0, -1);
908
- return Promise.resolve({ status: "reset-buf", newBuf: edited });
909
- }
910
- catch (err) {
911
- const msg = err instanceof Error ? err.message : String(err);
912
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
913
- return Promise.resolve({ status: "error" });
914
- }
915
- finally {
916
- if (dir) {
917
- try {
918
- rmSync(dir, { recursive: true, force: true });
919
- }
920
- catch {
921
- // Temp-dir cleanup is best-effort; a leftover dir is harmless.
922
- }
923
- }
924
- }
925
- },
719
+ * `\e` / `\edit [FILE] [LINE]` — edit the current query buffer (or a file)
720
+ * in the user's editor, then load the edited text back into the query
721
+ * buffer.
722
+ *
723
+ * This port implements the common no-FILE form: dump the current query
724
+ * buffer to a temp file, spawn the editor on it inheriting stdio (upstream
725
+ * `do_edit` → `editFile`), and on a clean exit read the file back and
726
+ * return it as the new query buffer via `status: 'reset-buf'`. Upstream
727
+ * strips a single trailing newline the editor may add; we do the same so
728
+ * round-tripping an unchanged buffer is a no-op.
729
+ *
730
+ * Editor selection follows {@link resolveEditor}. The spawn uses
731
+ * `spawnSync(..., { stdio: 'inherit' })` so the editor owns the terminal.
732
+ * If the editor exits non-zero (or fails to spawn) we leave the buffer
733
+ * untouched and report an error, matching upstream's behaviour of not
734
+ * importing a failed edit.
735
+ *
736
+ * FILE / LINE arguments are accepted but the buffer is still seeded from
737
+ * the current query buffer; a future WP can layer file-backed editing
738
+ * (`\e file`) and `\ef`/`\ev` on top.
739
+ */
740
+ const cmdEdit = {
741
+ name: "e",
742
+ aliases: ["edit"],
743
+ argMode: "whole-line",
744
+ helpKey: "e",
745
+ run: (ctx) => {
746
+ ctx.restOfLine();
747
+ const editor = resolveEditor();
748
+ const seed = ctx.queryBuf.length > 0 && !ctx.queryBuf.endsWith("\n") ? ctx.queryBuf + "\n" : ctx.queryBuf;
749
+ let dir = null;
750
+ try {
751
+ dir = mkdtempSync(join(tmpdir(), "psql.edit."));
752
+ const file = join(dir, "edit.sql");
753
+ writeFileSync(file, seed, "utf8");
754
+ const result = spawnSync(editor, [file], { stdio: "inherit" });
755
+ if (result.error || result.status !== null && result.status !== 0) {
756
+ const why = result.error ? result.error.message : `editor exited with status ${String(result.status)}`;
757
+ writeErr(`\\${ctx.cmdName}: ${why}\n`);
758
+ return Promise.resolve({ status: "error" });
759
+ }
760
+ let edited = readFileSync(file, "utf8");
761
+ if (edited.endsWith("\n")) edited = edited.slice(0, -1);
762
+ return Promise.resolve({
763
+ status: "reset-buf",
764
+ newBuf: edited
765
+ });
766
+ } catch (err) {
767
+ const msg = err instanceof Error ? err.message : String(err);
768
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
769
+ return Promise.resolve({ status: "error" });
770
+ } finally {
771
+ if (dir) try {
772
+ rmSync(dir, {
773
+ recursive: true,
774
+ force: true
775
+ });
776
+ } catch {}
777
+ }
778
+ }
926
779
  };
927
780
  /**
928
- * `\s [FILENAME]` — print the command-line history, or save it to FILENAME.
929
- *
930
- * Mirrors upstream `exec_command_s` / `printHistory`:
931
- *
932
- * - No argument: write the in-memory history (one entry per line) to
933
- * stdout. Multi-line entries are printed verbatim (with their embedded
934
- * newlines), matching readline's `\s` dump.
935
- * - FILENAME given: write the same dump to that file. On success, and
936
- * unless `\set QUIET` is in effect, print `Wrote history to file
937
- * "<file>".` to stdout. On failure, emit the OS error to stderr and
938
- * return an error.
939
- *
940
- * The history source is {@link getHistory}, the session's in-memory list
941
- * populated as each line is submitted (see `io/history.ts`).
942
- */
943
- export const cmdS = {
944
- name: "s",
945
- helpKey: "s",
946
- run: (ctx) => {
947
- const fname = ctx.nextArg("normal");
948
- const entries = getHistory();
949
- // Each entry is one logical command; readline's `\s` prints them one
950
- // per line, so a trailing newline per entry reproduces that layout.
951
- const body = entries.map((e) => e + "\n").join("");
952
- if (fname === null || fname.length === 0) {
953
- writeOut(body);
954
- return Promise.resolve({ status: "ok" });
955
- }
956
- try {
957
- writeFileSync(fname, body, "utf8");
958
- }
959
- catch (err) {
960
- const msg = err instanceof Error ? err.message : String(err);
961
- writeErr(`\\${ctx.cmdName}: ${msg}\n`);
962
- return Promise.resolve({ status: "error" });
963
- }
964
- if (!ctx.settings.quiet) {
965
- writeOut(`Wrote history to file "${fname}".\n`);
966
- }
967
- return Promise.resolve({ status: "ok" });
968
- },
781
+ * `\s [FILENAME]` — print the command-line history, or save it to FILENAME.
782
+ *
783
+ * Mirrors upstream `exec_command_s` / `printHistory`:
784
+ *
785
+ * - No argument: write the in-memory history (one entry per line) to
786
+ * stdout. Multi-line entries are printed verbatim (with their embedded
787
+ * newlines), matching readline's `\s` dump.
788
+ * - FILENAME given: write the same dump to that file. On success, and
789
+ * unless `\set QUIET` is in effect, print `Wrote history to file
790
+ * "<file>".` to stdout. On failure, emit the OS error to stderr and
791
+ * return an error.
792
+ *
793
+ * The history source is {@link getHistory}, the session's in-memory list
794
+ * populated as each line is submitted (see `io/history.ts`).
795
+ */
796
+ const cmdS = {
797
+ name: "s",
798
+ helpKey: "s",
799
+ run: (ctx) => {
800
+ const fname = ctx.nextArg("normal");
801
+ const body = getHistory().map((e) => e + "\n").join("");
802
+ if (fname === null || fname.length === 0) {
803
+ writeOut(body);
804
+ return Promise.resolve({ status: "ok" });
805
+ }
806
+ try {
807
+ writeFileSync(fname, body, "utf8");
808
+ } catch (err) {
809
+ const msg = err instanceof Error ? err.message : String(err);
810
+ writeErr(`\\${ctx.cmdName}: ${msg}\n`);
811
+ return Promise.resolve({ status: "error" });
812
+ }
813
+ if (!ctx.settings.quiet) writeOut(`Wrote history to file "${fname}".\n`);
814
+ return Promise.resolve({ status: "ok" });
815
+ }
969
816
  };
817
+ //#endregion
818
+ export { cmdCd, cmdCopyright, cmdEcho, cmdEdit, cmdErrverbose, cmdGetenv, cmdHelpSQL, cmdPrompt, cmdQecho, cmdQuit, cmdReset, cmdS, cmdSet, cmdSetenv, cmdShell, cmdSlashHelp, cmdTiming, cmdUnset, cmdWarn, formatErrorReport, psqlErrorPrefix, renderLineAndCaret, resolveEditor };