neonctl 2.22.2 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +84 -0
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/connection_string.js +9 -1
  5. package/commands/functions.js +277 -0
  6. package/commands/index.js +4 -0
  7. package/commands/neon_auth.js +1013 -0
  8. package/commands/projects.js +9 -1
  9. package/commands/psql.js +6 -1
  10. package/functions_api.js +44 -0
  11. package/package.json +15 -5
  12. package/psql/cli.js +51 -0
  13. package/psql/command/cmd_cond.js +437 -0
  14. package/psql/command/cmd_connect.js +815 -0
  15. package/psql/command/cmd_copy.js +1025 -0
  16. package/psql/command/cmd_describe.js +1810 -0
  17. package/psql/command/cmd_format.js +909 -0
  18. package/psql/command/cmd_io.js +2187 -0
  19. package/psql/command/cmd_lo.js +385 -0
  20. package/psql/command/cmd_meta.js +970 -0
  21. package/psql/command/cmd_misc.js +187 -0
  22. package/psql/command/cmd_pipeline.js +1141 -0
  23. package/psql/command/cmd_restrict.js +171 -0
  24. package/psql/command/cmd_show.js +751 -0
  25. package/psql/command/dispatch.js +343 -0
  26. package/psql/command/inputQueue.js +42 -0
  27. package/psql/command/shared.js +71 -0
  28. package/psql/complete/filenames.js +139 -0
  29. package/psql/complete/index.js +104 -0
  30. package/psql/complete/matcher.js +314 -0
  31. package/psql/complete/psqlVars.js +247 -0
  32. package/psql/complete/queries.js +491 -0
  33. package/psql/complete/rules.js +2387 -0
  34. package/psql/core/common.js +1250 -0
  35. package/psql/core/help.js +576 -0
  36. package/psql/core/mainloop.js +1353 -0
  37. package/psql/core/prompt.js +437 -0
  38. package/psql/core/settings.js +684 -0
  39. package/psql/core/sqlHelp.js +1066 -0
  40. package/psql/core/startup.js +840 -0
  41. package/psql/core/syncVars.js +116 -0
  42. package/psql/core/variables.js +287 -0
  43. package/psql/describe/formatters.js +1277 -0
  44. package/psql/describe/processNamePattern.js +270 -0
  45. package/psql/describe/queries.js +2373 -0
  46. package/psql/describe/versionGate.js +43 -0
  47. package/psql/index.js +2005 -0
  48. package/psql/io/history.js +299 -0
  49. package/psql/io/input.js +120 -0
  50. package/psql/io/lineEditor/buffer.js +323 -0
  51. package/psql/io/lineEditor/complete.js +227 -0
  52. package/psql/io/lineEditor/filename.js +159 -0
  53. package/psql/io/lineEditor/index.js +891 -0
  54. package/psql/io/lineEditor/keymap.js +738 -0
  55. package/psql/io/lineEditor/vt100.js +363 -0
  56. package/psql/io/pgpass.js +202 -0
  57. package/psql/io/pgservice.js +194 -0
  58. package/psql/io/psqlrc.js +422 -0
  59. package/psql/print/aligned.js +1756 -0
  60. package/psql/print/asciidoc.js +248 -0
  61. package/psql/print/crosstab.js +460 -0
  62. package/psql/print/csv.js +92 -0
  63. package/psql/print/html.js +258 -0
  64. package/psql/print/json.js +96 -0
  65. package/psql/print/latex.js +396 -0
  66. package/psql/print/pager.js +265 -0
  67. package/psql/print/troff.js +258 -0
  68. package/psql/print/unaligned.js +118 -0
  69. package/psql/print/units.js +135 -0
  70. package/psql/scanner/slash.js +513 -0
  71. package/psql/scanner/sql.js +910 -0
  72. package/psql/scanner/stringutils.js +390 -0
  73. package/psql/types/backslash.js +1 -0
  74. package/psql/types/connection.js +1 -0
  75. package/psql/types/index.js +7 -0
  76. package/psql/types/printer.js +1 -0
  77. package/psql/types/repl.js +1 -0
  78. package/psql/types/scanner.js +24 -0
  79. package/psql/types/settings.js +1 -0
  80. package/psql/types/variables.js +1 -0
  81. package/psql/wire/connection.js +2844 -0
  82. package/psql/wire/copy.js +108 -0
  83. package/psql/wire/notify.js +59 -0
  84. package/psql/wire/pipeline.js +519 -0
  85. package/psql/wire/protocol.js +466 -0
  86. package/psql/wire/sasl.js +296 -0
  87. package/psql/wire/tls.js +596 -0
  88. package/test_utils/fixtures.js +1 -0
  89. package/utils/esbuild.js +147 -0
  90. package/utils/psql.js +107 -11
  91. package/utils/zip.js +4 -0
  92. package/writer.js +1 -1
  93. package/commands/auth.test.js +0 -211
  94. package/commands/branches.test.js +0 -460
  95. package/commands/checkout.test.js +0 -170
  96. package/commands/connection_string.test.js +0 -196
  97. package/commands/data_api.test.js +0 -169
  98. package/commands/databases.test.js +0 -39
  99. package/commands/help.test.js +0 -9
  100. package/commands/init.test.js +0 -56
  101. package/commands/ip_allow.test.js +0 -59
  102. package/commands/link.test.js +0 -381
  103. package/commands/operations.test.js +0 -7
  104. package/commands/orgs.test.js +0 -7
  105. package/commands/projects.test.js +0 -144
  106. package/commands/psql.test.js +0 -49
  107. package/commands/roles.test.js +0 -37
  108. package/commands/set_context.test.js +0 -159
  109. package/commands/vpc_endpoints.test.js +0 -69
  110. package/context.test.js +0 -119
  111. package/env.test.js +0 -55
  112. package/utils/formats.test.js +0 -32
  113. package/writer.test.js +0 -104
@@ -0,0 +1,396 @@
1
+ import { formatNumericLocale } from './units.js';
2
+ /**
3
+ * LaTeX printers — `latex` (tabular) and `latex-longtable` (longtable).
4
+ *
5
+ * Mirrors print.c `print_latex_text`, `print_latex_vertical`, and
6
+ * `print_latex_longtable_text`. Expanded mode for both `latex` and
7
+ * `latex-longtable` falls through to the vertical renderer
8
+ * (`print_latex_vertical`) — that's how upstream dispatches.
9
+ *
10
+ * Border behavior (flat tabular, from print.c):
11
+ * - `topt.border` is clamped to 0..3.
12
+ * - tabular column spec gets ` | ` between columns when border > 0,
13
+ * and a leading/trailing `|` at border >= 2.
14
+ * - border == 2 emits `\hline` around header and at end of table.
15
+ * - border == 3 emits `\hline` after every row.
16
+ *
17
+ * Border behavior (expanded, from print.c):
18
+ * - `topt.border` is clamped to 0..2 (note: not 0..3).
19
+ * - tabular column spec is `cl`, `c|l`, or `|c|l|` for border 0/1/2.
20
+ * - border >= 1 emits `\hline` between records.
21
+ * - border == 2 wraps each "Record N" header in `\hline` lines.
22
+ *
23
+ * Numeric columns (per the OID heuristic) get the `r` alignment letter;
24
+ * everything else gets `l`. That letter is passed straight through to
25
+ * LaTeX's column spec.
26
+ *
27
+ * Escape: 14 LaTeX-special characters are rewritten. Embedded newlines
28
+ * in a cell turn into `\\` (LaTeX line break) for `latex`. The
29
+ * longtable variant uses the same escape but wraps cells in
30
+ * `\raggedright{...}` and ends rows with `\tabularnewline`.
31
+ */
32
+ // INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC, INTERVAL.
33
+ const NUMERIC_OIDS = new Set([21, 23, 20, 700, 701, 1700, 1186]);
34
+ export const latexPrinter = {
35
+ format: 'latex',
36
+ printQuery(rs, opts, out) {
37
+ const topt = opts.topt;
38
+ if (topt.expanded === 'on') {
39
+ return printLatexVertical(rs, opts, out);
40
+ }
41
+ return printLatexFlat(rs, opts, out);
42
+ },
43
+ };
44
+ const printLatexFlat = (rs, opts, out) => {
45
+ const topt = opts.topt;
46
+ const tuplesOnly = topt.tuplesOnly;
47
+ const startTable = topt.startTable;
48
+ const stopTable = topt.stopTable;
49
+ const border = clampBorder(topt.border, 3);
50
+ const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
51
+ const title = opts.title ?? topt.title;
52
+ const footers = opts.footers ?? topt.footers;
53
+ const headers = rs.fields.map((f) => f.name);
54
+ const ncols = rs.fields.length;
55
+ const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'r' : 'l');
56
+ const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
57
+ let buf = '';
58
+ if (startTable) {
59
+ if (!tuplesOnly && title) {
60
+ buf += '\\begin{center}\n';
61
+ buf += escapeLatex(title);
62
+ buf += '\n\\end{center}\n\n';
63
+ }
64
+ buf += '\\begin{tabular}{';
65
+ if (border >= 2)
66
+ buf += '| ';
67
+ aligns.forEach((a, idx) => {
68
+ buf += a;
69
+ if (border !== 0 && idx < ncols - 1)
70
+ buf += ' | ';
71
+ });
72
+ if (border >= 2)
73
+ buf += ' |';
74
+ buf += '}\n';
75
+ if (!tuplesOnly && border >= 2)
76
+ buf += '\\hline\n';
77
+ if (!tuplesOnly) {
78
+ headers.forEach((h, idx) => {
79
+ if (idx !== 0)
80
+ buf += ' & ';
81
+ buf += '\\textit{' + escapeLatex(h) + '}';
82
+ });
83
+ buf += ' \\\\\n';
84
+ buf += '\\hline\n';
85
+ }
86
+ }
87
+ cells.forEach((row) => {
88
+ row.forEach((value, idx) => {
89
+ buf += escapeLatex(value);
90
+ if (idx === ncols - 1) {
91
+ buf += ' \\\\\n';
92
+ if (border === 3)
93
+ buf += '\\hline\n';
94
+ }
95
+ else {
96
+ buf += ' & ';
97
+ }
98
+ });
99
+ });
100
+ if (stopTable) {
101
+ if (border === 2)
102
+ buf += '\\hline\n';
103
+ buf += '\\end{tabular}\n\n\\noindent ';
104
+ if (!tuplesOnly) {
105
+ const effective = effectiveFooters(rs, topt, footers);
106
+ for (const f of effective) {
107
+ buf += escapeLatex(f) + ' \\\\\n';
108
+ }
109
+ }
110
+ buf += '\n';
111
+ }
112
+ out.write(buf);
113
+ return Promise.resolve();
114
+ };
115
+ const printLatexVertical = (rs, opts, out) => {
116
+ const topt = opts.topt;
117
+ const tuplesOnly = topt.tuplesOnly;
118
+ const startTable = topt.startTable;
119
+ const stopTable = topt.stopTable;
120
+ const border = clampBorder(topt.border, 2);
121
+ const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
122
+ const title = opts.title ?? topt.title;
123
+ const footers = opts.footers ?? topt.footers;
124
+ const headers = rs.fields.map((f) => f.name);
125
+ const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
126
+ let buf = '';
127
+ if (startTable) {
128
+ if (!tuplesOnly && title) {
129
+ buf += '\\begin{center}\n';
130
+ buf += escapeLatex(title);
131
+ buf += '\n\\end{center}\n\n';
132
+ }
133
+ buf += '\\begin{tabular}{';
134
+ if (border === 0)
135
+ buf += 'cl';
136
+ else if (border === 1)
137
+ buf += 'c|l';
138
+ else
139
+ buf += '|c|l|';
140
+ buf += '}\n';
141
+ }
142
+ let record = topt.prior + 1;
143
+ cells.forEach((row) => {
144
+ if (!tuplesOnly) {
145
+ if (border === 2) {
146
+ buf += '\\hline\n';
147
+ buf += `\\multicolumn{2}{|c|}{\\textit{Record ${String(record)}}} \\\\\n`;
148
+ }
149
+ else {
150
+ buf += `\\multicolumn{2}{c}{\\textit{Record ${String(record)}}} \\\\\n`;
151
+ }
152
+ record += 1;
153
+ }
154
+ if (border >= 1)
155
+ buf += '\\hline\n';
156
+ row.forEach((value, idx) => {
157
+ buf += escapeLatex(headers[idx]);
158
+ buf += ' & ';
159
+ buf += escapeLatex(value);
160
+ buf += ' \\\\\n';
161
+ });
162
+ });
163
+ if (stopTable) {
164
+ if (border === 2)
165
+ buf += '\\hline\n';
166
+ buf += '\\end{tabular}\n\n\\noindent ';
167
+ // Expanded mode does NOT include the default "(N rows)" footer;
168
+ // only user-supplied footers (matches print_latex_vertical).
169
+ if (!tuplesOnly && footers && footers.length > 0) {
170
+ for (const f of footers) {
171
+ buf += escapeLatex(f) + ' \\\\\n';
172
+ }
173
+ }
174
+ buf += '\n';
175
+ }
176
+ out.write(buf);
177
+ return Promise.resolve();
178
+ };
179
+ export const latexLongtablePrinter = {
180
+ format: 'latex-longtable',
181
+ printQuery(rs, opts, out) {
182
+ // Upstream dispatch sends both `latex` and `latex-longtable` to
183
+ // `print_latex_vertical` when expanded is on (cf. print.c).
184
+ if (opts.topt.expanded === 'on') {
185
+ return printLatexVertical(rs, opts, out);
186
+ }
187
+ const topt = opts.topt;
188
+ const tuplesOnly = topt.tuplesOnly;
189
+ const startTable = topt.startTable;
190
+ const stopTable = topt.stopTable;
191
+ const border = clampBorder(topt.border, 3);
192
+ const nullPrint = opts.nullPrint !== '' ? opts.nullPrint : topt.nullPrint;
193
+ const title = opts.title ?? topt.title;
194
+ const headers = rs.fields.map((f) => f.name);
195
+ const ncols = rs.fields.length;
196
+ const aligns = rs.fields.map((f) => NUMERIC_OIDS.has(f.dataTypeID) ? 'r' : 'l');
197
+ const cells = rs.rows.map((row) => row.map((cell) => renderCell(cell, nullPrint, topt.numericLocale)));
198
+ // `topt.tableAttr` for longtable encodes per-column widths in a
199
+ // whitespace-separated list, consumed left-to-right with a fall
200
+ // back to the previous value once exhausted.
201
+ const widths = (topt.tableAttr ?? '')
202
+ .split(/[\s]+/)
203
+ .filter((w) => w !== '');
204
+ let widthCursor = 0;
205
+ let lastWidth = null;
206
+ let buf = '';
207
+ if (startTable) {
208
+ buf += '\\begin{longtable}{';
209
+ if (border >= 2)
210
+ buf += '| ';
211
+ aligns.forEach((a, idx) => {
212
+ if (a === 'l' && widths.length > 0) {
213
+ let w = null;
214
+ if (widthCursor < widths.length) {
215
+ w = widths[widthCursor];
216
+ widthCursor += 1;
217
+ lastWidth = w;
218
+ }
219
+ else if (lastWidth !== null) {
220
+ w = lastWidth;
221
+ }
222
+ if (w !== null) {
223
+ buf += `p{${w}\\textwidth}`;
224
+ }
225
+ else {
226
+ buf += 'l';
227
+ }
228
+ }
229
+ else {
230
+ buf += a;
231
+ }
232
+ if (border !== 0 && idx < ncols - 1)
233
+ buf += ' | ';
234
+ });
235
+ if (border >= 2)
236
+ buf += ' |';
237
+ buf += '}\n';
238
+ if (!tuplesOnly) {
239
+ // firsthead
240
+ if (border >= 2)
241
+ buf += '\\toprule\n';
242
+ headers.forEach((h, idx) => {
243
+ if (idx !== 0)
244
+ buf += ' & ';
245
+ buf += '\\small\\textbf{\\textit{' + escapeLatex(h) + '}}';
246
+ });
247
+ buf += ' \\\\\n';
248
+ buf += '\\midrule\n\\endfirsthead\n';
249
+ // continuation heads
250
+ if (border >= 2)
251
+ buf += '\\toprule\n';
252
+ headers.forEach((h, idx) => {
253
+ if (idx !== 0)
254
+ buf += ' & ';
255
+ buf += '\\small\\textbf{\\textit{' + escapeLatex(h) + '}}';
256
+ });
257
+ buf += ' \\\\\n';
258
+ if (border !== 3)
259
+ buf += '\\midrule\n';
260
+ buf += '\\endhead\n';
261
+ if (title) {
262
+ if (border === 2)
263
+ buf += '\\bottomrule\n';
264
+ buf +=
265
+ '\\caption[' +
266
+ escapeLatex(title) +
267
+ ' (Continued)]{' +
268
+ escapeLatex(title) +
269
+ '}\n\\endfoot\n';
270
+ if (border === 2)
271
+ buf += '\\bottomrule\n';
272
+ buf +=
273
+ '\\caption[' +
274
+ escapeLatex(title) +
275
+ ']{' +
276
+ escapeLatex(title) +
277
+ '}\n\\endlastfoot\n';
278
+ }
279
+ else if (border >= 2) {
280
+ buf += '\\bottomrule\n\\endfoot\n';
281
+ buf += '\\bottomrule\n\\endlastfoot\n';
282
+ }
283
+ }
284
+ }
285
+ // Cells. Upstream interleaves `\n&\n` between in-row cells (and
286
+ // emits `\tabularnewline` to end a row), wrapping each value in
287
+ // `\raggedright{...}`.
288
+ let cellIdx = 0;
289
+ cells.forEach((row) => {
290
+ row.forEach((value) => {
291
+ if (cellIdx !== 0 && cellIdx % ncols !== 0)
292
+ buf += '\n&\n';
293
+ buf += '\\raggedright{' + escapeLatex(value) + '}';
294
+ if ((cellIdx + 1) % ncols === 0) {
295
+ buf += ' \\tabularnewline\n';
296
+ if (border === 3)
297
+ buf += ' \\hline\n';
298
+ }
299
+ cellIdx += 1;
300
+ });
301
+ });
302
+ if (stopTable)
303
+ buf += '\\end{longtable}\n';
304
+ out.write(buf);
305
+ return Promise.resolve();
306
+ },
307
+ };
308
+ const clampBorder = (b, max) => {
309
+ if (b > max)
310
+ return max;
311
+ if (b < 0)
312
+ return 0;
313
+ return b;
314
+ };
315
+ const effectiveFooters = (rs, topt, footers) => {
316
+ if (footers && footers.length > 0)
317
+ return footers;
318
+ if (topt.defaultFooter) {
319
+ const n = rs.rows.length;
320
+ return [`(${String(n)} ${n === 1 ? 'row' : 'rows'})`];
321
+ }
322
+ return [];
323
+ };
324
+ const escapeLatex = (input) => {
325
+ let out = '';
326
+ for (const ch of input) {
327
+ switch (ch) {
328
+ case '#':
329
+ out += '\\#';
330
+ break;
331
+ case '$':
332
+ out += '\\$';
333
+ break;
334
+ case '%':
335
+ out += '\\%';
336
+ break;
337
+ case '&':
338
+ out += '\\&';
339
+ break;
340
+ case '<':
341
+ out += '\\textless{}';
342
+ break;
343
+ case '>':
344
+ out += '\\textgreater{}';
345
+ break;
346
+ case '\\':
347
+ out += '\\textbackslash{}';
348
+ break;
349
+ case '^':
350
+ out += '\\^{}';
351
+ break;
352
+ case '_':
353
+ out += '\\_';
354
+ break;
355
+ case '{':
356
+ out += '\\{';
357
+ break;
358
+ case '|':
359
+ out += '\\textbar{}';
360
+ break;
361
+ case '}':
362
+ out += '\\}';
363
+ break;
364
+ case '~':
365
+ out += '\\~{}';
366
+ break;
367
+ case '\n':
368
+ out += '\\\\';
369
+ break;
370
+ default:
371
+ out += ch;
372
+ }
373
+ }
374
+ return out;
375
+ };
376
+ const renderCell = (cell, nullPrint, numericLocale) => {
377
+ if (cell === null || cell === undefined)
378
+ return nullPrint;
379
+ if (typeof cell === 'string') {
380
+ return formatNumericLocale(cell, numericLocale);
381
+ }
382
+ if (typeof cell === 'number' || typeof cell === 'bigint') {
383
+ return formatNumericLocale(cell.toString(), numericLocale);
384
+ }
385
+ if (typeof cell === 'boolean')
386
+ return cell ? 't' : 'f';
387
+ if (cell instanceof Date)
388
+ return cell.toISOString();
389
+ if (cell instanceof Uint8Array) {
390
+ let hex = '\\x';
391
+ for (const b of cell)
392
+ hex += b.toString(16).padStart(2, '0');
393
+ return hex;
394
+ }
395
+ return JSON.stringify(cell);
396
+ };
@@ -0,0 +1,265 @@
1
+ import { spawn } from 'child_process';
2
+ import { accessSync, constants as fsConstants } from 'node:fs';
3
+ import { basename, delimiter, isAbsolute, join } from 'path';
4
+ /**
5
+ * Can `command` be spawned with `shell:false`? A bare name is searched on
6
+ * `$PATH`; a path is checked directly. Used to avoid `spawn(missingBinary,
7
+ * { shell:false })`, which does NOT throw synchronously (our try/catch never
8
+ * fires) but emits an async ENOENT — leaving writes to the dead child's stdin
9
+ * to vanish into a blank terminal (review item #19).
10
+ */
11
+ const pagerCommandResolvable = (command) => {
12
+ const ok = (p) => {
13
+ try {
14
+ accessSync(p, fsConstants.X_OK);
15
+ return true;
16
+ }
17
+ catch {
18
+ // On Windows X_OK is not meaningful and a bare name omits `.exe`; fall
19
+ // back to a plain existence probe with common executable extensions.
20
+ if (process.platform === 'win32') {
21
+ for (const ext of ['', '.exe', '.cmd', '.bat']) {
22
+ try {
23
+ accessSync(p + ext, fsConstants.F_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ /* keep trying */
28
+ }
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ };
34
+ if (command.includes('/') || command.includes('\\') || isAbsolute(command)) {
35
+ return ok(command);
36
+ }
37
+ return (process.env.PATH ?? '')
38
+ .split(delimiter)
39
+ .filter((d) => d.length > 0)
40
+ .some((dir) => ok(join(dir, command)));
41
+ };
42
+ /**
43
+ * Resolve the pager command string. Mirrors upstream:
44
+ * PSQL_PAGER → PAGER → DEFAULT_PAGER (`less` on POSIX, none on Windows).
45
+ *
46
+ * Empty / whitespace-only env values FALL THROUGH to the next candidate. This
47
+ * deliberately diverges from strict upstream (where `PSQL_PAGER=''` would
48
+ * disable the pager outright) because in Node a spawned child cannot easily
49
+ * "unset" an inherited env var — tests have to override it with the empty
50
+ * string. Treating empty values as "unset" matches the conformance spec
51
+ * (`tests/psql-conformance/tap/030_pager.spec.ts`) and lets `PSQL_PAGER=''`
52
+ * fall through to PAGER, which is the user-friendly interpretation.
53
+ *
54
+ * Users who want to disable the pager unconditionally should set
55
+ * `\pset pager off` (preferred) or unset both env vars before launch.
56
+ */
57
+ const resolvePagerCmd = (opts) => {
58
+ const env = opts.env ?? process.env;
59
+ const candidates = [
60
+ opts.pagerCmd,
61
+ env.PSQL_PAGER,
62
+ env.PAGER,
63
+ ];
64
+ for (const c of candidates) {
65
+ if (c === undefined)
66
+ continue;
67
+ // Empty or whitespace-only → treat as "not set" and try the next slot.
68
+ if (/^\s*$/.test(c))
69
+ continue;
70
+ return c;
71
+ }
72
+ // DEFAULT_PAGER: `less` on POSIX; nothing on Windows.
73
+ if (process.platform === 'win32')
74
+ return '';
75
+ return 'less';
76
+ };
77
+ const getIsTty = (opts) => {
78
+ if (opts.isTty !== undefined)
79
+ return opts.isTty;
80
+ const stream = opts.stdout ?? process.stdout;
81
+ // Some test streams won't have isTTY.
82
+ const tty = stream.isTTY;
83
+ return Boolean(tty);
84
+ };
85
+ const getTerminalHeight = (opts) => {
86
+ if (opts.terminalHeight !== undefined)
87
+ return opts.terminalHeight;
88
+ const stream = opts.stdout ?? process.stdout;
89
+ const rows = stream.rows;
90
+ // If we can't tell, fall back to 24 (classic VT100 default).
91
+ return typeof rows === 'number' && rows > 0 ? rows : 24;
92
+ };
93
+ /** Standalone helper to determine whether a pager is needed at all. */
94
+ export const isPagerNeeded = (opts) => {
95
+ if (opts.pager === 'off')
96
+ return false;
97
+ const cmd = resolvePagerCmd(opts);
98
+ if (cmd === '')
99
+ return false;
100
+ if (opts.pager === 'always')
101
+ return true;
102
+ // pager === 'on'
103
+ if (!getIsTty(opts))
104
+ return false;
105
+ if (opts.lines === undefined)
106
+ return false;
107
+ const threshold = Math.max(opts.pagerMinLines, getTerminalHeight(opts));
108
+ return opts.lines >= threshold;
109
+ };
110
+ /**
111
+ * Decide whether a result of `rowCount` rows by `colCount` columns should be
112
+ * routed through the pager when written to `output`.
113
+ *
114
+ * NOTE on `pager === 'always'`: the TTY check is INTENTIONALLY skipped in
115
+ * this case. Upstream's `\pset pager always` is a user-explicit "force the
116
+ * pager on" override; honouring it on a pipe matches the integration test
117
+ * harness contract (see `tests/psql-conformance/tap/030_pager.spec.ts`) where
118
+ * the child process has no controlling TTY but the spec still requires the
119
+ * configured PAGER to be invoked. For `pager === 'on'` (auto mode) we keep
120
+ * the TTY guard so non-interactive runs don't spuriously spawn `less`.
121
+ */
122
+ export const shouldPage = (opts) => {
123
+ if (opts.pager === 'off')
124
+ return false;
125
+ if (opts.redirectedOutput)
126
+ return false;
127
+ // Rough heuristic for "lines" — header (3) + rows + footer (1). Matches
128
+ // upstream `IsPagerNeeded` which counts rendered table lines.
129
+ const HEADER_LINES = 3;
130
+ const FOOTER_LINES = 1;
131
+ const estimatedLines = HEADER_LINES + Math.max(0, opts.rowCount) + FOOTER_LINES;
132
+ // TTY check: explicit override wins, else inspect the stream's own isTTY.
133
+ const isTty = opts.isTty !== undefined
134
+ ? opts.isTty
135
+ : Boolean(opts.output.isTTY);
136
+ // `pager === 'always'` bypasses the TTY guard — see the docstring above.
137
+ // The remaining gates (resolved cmd, redirected output, off) are enforced
138
+ // inside `isPagerNeeded`.
139
+ if (opts.pager !== 'always' && !isTty)
140
+ return false;
141
+ return isPagerNeeded({
142
+ pager: opts.pager,
143
+ pagerMinLines: opts.pagerMinLines,
144
+ pagerCmd: opts.pagerCmd,
145
+ env: opts.env,
146
+ stdout: opts.output,
147
+ isTty,
148
+ terminalHeight: opts.terminalHeight,
149
+ lines: estimatedLines,
150
+ });
151
+ };
152
+ const SHELL_META = /[\s|;><]/;
153
+ const parsePagerCmd = (cmd) => {
154
+ // Match upstream behavior: when the value looks shell-y, hand it off to
155
+ // /bin/sh -c. Otherwise treat it as a direct argv[0].
156
+ if (SHELL_META.test(cmd)) {
157
+ return { command: cmd, args: [], shell: true };
158
+ }
159
+ return { command: cmd, args: [], shell: false };
160
+ };
161
+ const buildPagerEnv = (cmd, baseEnv) => {
162
+ const env = { ...baseEnv };
163
+ // psql sets LESS=FRX by default; mirror it when the resolved pager is
164
+ // `less` and the caller hasn't already set LESS.
165
+ if (env.LESS === undefined) {
166
+ // Pull out the first whitespace-separated token to detect `less` even
167
+ // when args follow (e.g. "less -S").
168
+ const firstToken = cmd.trim().split(/\s+/, 1)[0] ?? '';
169
+ const program = basename(firstToken);
170
+ if (program === 'less') {
171
+ env.LESS = 'FRX';
172
+ }
173
+ }
174
+ return env;
175
+ };
176
+ const noOpHandle = (out) => ({
177
+ out,
178
+ spawned: false,
179
+ close: () => Promise.resolve(0),
180
+ });
181
+ /**
182
+ * Returns a PagerHandle. Caller writes data to `out`, then calls `close()`.
183
+ * If no pager spawned (pager='off', not a TTY, or fewer lines than threshold),
184
+ * `out` is `stdout`.
185
+ */
186
+ export const openPager = (opts) => {
187
+ const stdout = opts.stdout ?? process.stdout;
188
+ if (!isPagerNeeded(opts)) {
189
+ return noOpHandle(stdout);
190
+ }
191
+ const cmd = resolvePagerCmd(opts);
192
+ // isPagerNeeded already verified cmd is non-empty, but guard for safety.
193
+ if (cmd === '') {
194
+ return noOpHandle(stdout);
195
+ }
196
+ const { command, shell } = parsePagerCmd(cmd);
197
+ // A missing pager binary with shell:false emits an async ENOENT that the
198
+ // try/catch below cannot catch, and the result would be silently-discarded
199
+ // output. Pre-check and fall back to stdout instead (review item #19).
200
+ // shell:true goes through `sh -c`, which always exists.
201
+ if (!shell && !pagerCommandResolvable(command)) {
202
+ return noOpHandle(stdout);
203
+ }
204
+ const baseEnv = opts.env ?? process.env;
205
+ const childEnv = buildPagerEnv(cmd, baseEnv);
206
+ let child;
207
+ try {
208
+ child = spawn(command, [], {
209
+ stdio: ['pipe', 'inherit', 'inherit'],
210
+ shell,
211
+ env: childEnv,
212
+ });
213
+ }
214
+ catch {
215
+ // If the pager fails to spawn, fall back to stdout (matches upstream:
216
+ // `if (pagerpipe) return pagerpipe; restore_sigpipe_trap(); ... return stdout`).
217
+ return noOpHandle(stdout);
218
+ }
219
+ const stdin = child.stdin;
220
+ if (stdin === null) {
221
+ // Should not happen given stdio: ['pipe', ...], but be defensive.
222
+ return noOpHandle(stdout);
223
+ }
224
+ // Swallow EPIPE: the user can quit the pager early, after which any
225
+ // pending writes will fail with EPIPE. Upstream relies on SIGPIPE being
226
+ // ignored to short-circuit the write loop; we just drop the error.
227
+ stdin.on('error', (err) => {
228
+ if (err.code !== 'EPIPE') {
229
+ // Re-throw anything unexpected.
230
+ throw err;
231
+ }
232
+ });
233
+ const exitPromise = new Promise((resolve) => {
234
+ const settle = (code) => {
235
+ resolve(code ?? 0);
236
+ };
237
+ child.once('exit', (code) => {
238
+ settle(code);
239
+ });
240
+ child.once('error', () => {
241
+ // If the child errored (e.g. ENOENT), surface a non-zero exit code
242
+ // but don't throw — the caller's writes will have hit EPIPE which
243
+ // we already swallow.
244
+ settle(127);
245
+ });
246
+ });
247
+ return {
248
+ out: stdin,
249
+ spawned: true,
250
+ close: () => {
251
+ // End stdin then wait for the pager to drain & exit.
252
+ if (!stdin.writableEnded) {
253
+ try {
254
+ stdin.end();
255
+ }
256
+ catch (err) {
257
+ const e = err;
258
+ if (e.code !== 'EPIPE')
259
+ throw err;
260
+ }
261
+ }
262
+ return exitPromise;
263
+ },
264
+ };
265
+ };