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,42 +1,44 @@
1
+ //#region src/psql/command/inputQueue.ts
1
2
  /**
2
- * Pending-input queue for `\i` / `\include`.
3
- *
4
- * psql's `process_file()` switches the input source for the duration of an
5
- * included file: while a file is being processed, every subsequent input
6
- * line comes from the file, not the terminal. The natural place for that
7
- * switch is the mainloop (`MainLoop` in upstream `mainloop.c`).
8
- *
9
- * For WP-15 we keep `src/psql/core/mainloop.ts` untouched. Instead we expose
10
- * a tiny module-local queue. `\i` enqueues the contents of the included
11
- * file via {@link enqueue}; a future WP modifies the mainloop's line-source
12
- * to drain from {@link consumeNext} before reading more user input.
13
- *
14
- * Behaviour:
15
- *
16
- * - The queue stores file contents as raw strings (typically containing
17
- * multiple newline-separated SQL statements). Order is FIFO.
18
- * - {@link consumeNext} returns the head, or `null` if the queue is empty.
19
- * - {@link reset} clears the queue (used by tests and by any future error
20
- * recovery path that wants to abandon pending input).
21
- *
22
- * The queue is module-scoped because it represents the include stack of a
23
- * single REPL. Tests should always call {@link reset} in their afterEach so
24
- * a leftover entry doesn't contaminate the next test.
25
- */
3
+ * Pending-input queue for `\i` / `\include`.
4
+ *
5
+ * psql's `process_file()` switches the input source for the duration of an
6
+ * included file: while a file is being processed, every subsequent input
7
+ * line comes from the file, not the terminal. The natural place for that
8
+ * switch is the mainloop (`MainLoop` in upstream `mainloop.c`).
9
+ *
10
+ * For WP-15 we keep `src/psql/core/mainloop.ts` untouched. Instead we expose
11
+ * a tiny module-local queue. `\i` enqueues the contents of the included
12
+ * file via {@link enqueue}; a future WP modifies the mainloop's line-source
13
+ * to drain from {@link consumeNext} before reading more user input.
14
+ *
15
+ * Behaviour:
16
+ *
17
+ * - The queue stores file contents as raw strings (typically containing
18
+ * multiple newline-separated SQL statements). Order is FIFO.
19
+ * - {@link consumeNext} returns the head, or `null` if the queue is empty.
20
+ * - {@link reset} clears the queue (used by tests and by any future error
21
+ * recovery path that wants to abandon pending input).
22
+ *
23
+ * The queue is module-scoped because it represents the include stack of a
24
+ * single REPL. Tests should always call {@link reset} in their afterEach so
25
+ * a leftover entry doesn't contaminate the next test.
26
+ */
26
27
  const pending = [];
27
28
  /** Append a string of input to the back of the queue. */
28
- export const enqueue = (content) => {
29
- pending.push(content);
29
+ const enqueue = (content) => {
30
+ pending.push(content);
30
31
  };
31
32
  /** Return and remove the next pending input, or `null` if none. */
32
- export const consumeNext = () => {
33
- if (pending.length === 0)
34
- return null;
35
- return pending.shift() ?? null;
33
+ const consumeNext = () => {
34
+ if (pending.length === 0) return null;
35
+ return pending.shift() ?? null;
36
36
  };
37
37
  /** Number of items currently in the queue. */
38
- export const size = () => pending.length;
38
+ const size = () => pending.length;
39
39
  /** Empty the queue. Tests should call this in cleanup. */
40
- export const reset = () => {
41
- pending.length = 0;
40
+ const reset = () => {
41
+ pending.length = 0;
42
42
  };
43
+ //#endregion
44
+ export { consumeNext, enqueue, reset, size };
@@ -1,71 +1,57 @@
1
+ //#region src/psql/command/shared.ts
1
2
  /**
2
- * Internal helpers shared by `cmd_meta.ts` and `cmd_format.ts`.
3
- *
4
- * Kept deliberately tiny: just stream writes and the boolean coercion
5
- * shared by `\timing`, `\t`, `\x`, and `\pset` toggles. The implementations
6
- * mirror the relevant pieces of upstream `command.c` (`ParseVariableBool`)
7
- * and `print.c` / `print_aligned*` without depending on either.
8
- *
9
- * Why a shared file: the WP spec asks for cmd-isolated test factories, not
10
- * for the command implementations themselves to duplicate one-line
11
- * primitives. Going through these helpers also keeps the `noConsole`
12
- * lint rule satisfied — every write touches `process.stdout` /
13
- * `process.stderr` directly rather than `console.log` / `console.error`.
14
- */
3
+ * Internal helpers shared by `cmd_meta.ts` and `cmd_format.ts`.
4
+ *
5
+ * Kept deliberately tiny: just stream writes and the boolean coercion
6
+ * shared by `\timing`, `\t`, `\x`, and `\pset` toggles. The implementations
7
+ * mirror the relevant pieces of upstream `command.c` (`ParseVariableBool`)
8
+ * and `print.c` / `print_aligned*` without depending on either.
9
+ *
10
+ * Why a shared file: the WP spec asks for cmd-isolated test factories, not
11
+ * for the command implementations themselves to duplicate one-line
12
+ * primitives. Going through these helpers also keeps the `noConsole`
13
+ * lint rule satisfied — every write touches `process.stdout` /
14
+ * `process.stderr` directly rather than `console.log` / `console.error`.
15
+ */
15
16
  /** Write to stdout. */
16
- export const writeOut = (s) => {
17
- process.stdout.write(s);
17
+ const writeOut = (s) => {
18
+ process.stdout.write(s);
18
19
  };
19
20
  /** Write to stderr. */
20
- export const writeErr = (s) => {
21
- process.stderr.write(s);
21
+ const writeErr = (s) => {
22
+ process.stderr.write(s);
22
23
  };
23
24
  /**
24
- * Parse a psql boolean the way `ParseVariableBool` does — case-insensitive
25
- * unique-prefix match against `true|false|yes|no|on|off`, plus `1`/`0`.
26
- *
27
- * Returns `null` for unrecognised input.
28
- */
29
- export const parseBool = (raw) => {
30
- if (raw.length === 0)
31
- return null;
32
- const lower = raw.toLowerCase();
33
- const startsWith = (target) => lower.length <= target.length && target.startsWith(lower);
34
- if (startsWith("true"))
35
- return true;
36
- if (startsWith("false"))
37
- return false;
38
- if (startsWith("yes"))
39
- return true;
40
- if (startsWith("no"))
41
- return false;
42
- if (lower.length >= 2) {
43
- if ("on".startsWith(lower))
44
- return true;
45
- if ("off".startsWith(lower))
46
- return false;
47
- }
48
- if (raw === "1")
49
- return true;
50
- if (raw === "0")
51
- return false;
52
- return null;
25
+ * Parse a psql boolean the way `ParseVariableBool` does — case-insensitive
26
+ * unique-prefix match against `true|false|yes|no|on|off`, plus `1`/`0`.
27
+ *
28
+ * Returns `null` for unrecognised input.
29
+ */
30
+ const parseBool = (raw) => {
31
+ if (raw.length === 0) return null;
32
+ const lower = raw.toLowerCase();
33
+ const startsWith = (target) => lower.length <= target.length && target.startsWith(lower);
34
+ if (startsWith("true")) return true;
35
+ if (startsWith("false")) return false;
36
+ if (startsWith("yes")) return true;
37
+ if (startsWith("no")) return false;
38
+ if (lower.length >= 2) {
39
+ if ("on".startsWith(lower)) return true;
40
+ if ("off".startsWith(lower)) return false;
41
+ }
42
+ if (raw === "1") return true;
43
+ if (raw === "0") return false;
44
+ return null;
53
45
  };
54
- export const parseTriple = (raw) => {
55
- const lower = raw.toLowerCase();
56
- if (lower.length === 0)
57
- return null;
58
- // Resolve booleans FIRST. Otherwise `t` matched the `toggle` prefix before
59
- // parseBool, so `\x t` toggled rather than turning expanded ON, and `\pset`
60
- // bool prefixes were inverted (review: minor divergences).
61
- const b = parseBool(raw);
62
- if (b === true)
63
- return "on";
64
- if (b === false)
65
- return "off";
66
- if ("auto".startsWith(lower))
67
- return "auto";
68
- if ("toggle".startsWith(lower))
69
- return "toggle";
70
- return null;
46
+ const parseTriple = (raw) => {
47
+ const lower = raw.toLowerCase();
48
+ if (lower.length === 0) return null;
49
+ const b = parseBool(raw);
50
+ if (b === true) return "on";
51
+ if (b === false) return "off";
52
+ if ("auto".startsWith(lower)) return "auto";
53
+ if ("toggle".startsWith(lower)) return "toggle";
54
+ return null;
71
55
  };
56
+ //#endregion
57
+ export { parseBool, parseTriple, writeErr, writeOut };
@@ -1,139 +1,96 @@
1
- /**
2
- * Filesystem-driven completion candidates for `\lo_*`, `\copy ... FROM/TO`,
3
- * and SQL `COPY ... FROM/TO`.
4
- *
5
- * Upstream psql implements this via readline's `rl_filename_completion_function`
6
- * plus a couple of custom `complete_from_files*` wrappers. We don't have
7
- * readline; we re-implement the bits we need with `fs.readdirSync`.
8
- *
9
- * The completer enumerates entries in the directory referenced by the
10
- * partial input, filters by basename prefix, and returns *full* candidates
11
- * (path + basename, matching what the user typed). Directories get a
12
- * trailing `/` so the editor's `shouldAppendSpace` keeps the user typing
13
- * through them.
14
- *
15
- * For the SQL `COPY ... FROM/TO` context — where the filename must be a
16
- * string literal — the candidates are wrapped in single quotes. Closing
17
- * quotes are added only when the candidate is a final filename (unique
18
- * match), so the line editor's "balanced quotes → append space" rule
19
- * fires; partial multi-candidate prefixes leave the closing quote off so
20
- * the user can keep typing.
21
- */
22
1
  import { readdirSync, statSync } from "node:fs";
23
2
  import { basename, dirname, join } from "node:path";
3
+ //#region src/psql/complete/filenames.ts
4
+ /**
5
+ * Filesystem-driven completion candidates for `\lo_*`, `\copy ... FROM/TO`,
6
+ * and SQL `COPY ... FROM/TO`.
7
+ *
8
+ * Upstream psql implements this via readline's `rl_filename_completion_function`
9
+ * plus a couple of custom `complete_from_files*` wrappers. We don't have
10
+ * readline; we re-implement the bits we need with `fs.readdirSync`.
11
+ *
12
+ * The completer enumerates entries in the directory referenced by the
13
+ * partial input, filters by basename prefix, and returns *full* candidates
14
+ * (path + basename, matching what the user typed). Directories get a
15
+ * trailing `/` so the editor's `shouldAppendSpace` keeps the user typing
16
+ * through them.
17
+ *
18
+ * For the SQL `COPY ... FROM/TO` context — where the filename must be a
19
+ * string literal — the candidates are wrapped in single quotes. Closing
20
+ * quotes are added only when the candidate is a final filename (unique
21
+ * match), so the line editor's "balanced quotes → append space" rule
22
+ * fires; partial multi-candidate prefixes leave the closing quote off so
23
+ * the user can keep typing.
24
+ */
24
25
  /**
25
- * Enumerate filesystem entries matching the partial path the user typed.
26
- *
27
- * `currentWord` is the raw token (post-tokenizer). It may contain a leading
28
- * `'` (when the user is already inside a single-quoted SQL literal). The
29
- * function strips that, looks up the dirname/basename, and returns full
30
- * candidates that the line editor can splice in with `replaceLength = code
31
- * points in currentWord`.
32
- *
33
- * Returns an empty array on any filesystem error (e.g. directory doesn't
34
- * exist, no read permission). Tab completion is best-effort — failing to
35
- * complete is the same as "no candidates".
36
- */
37
- export const completeFilenames = (currentWord, quoteCtx, cwd = process.cwd()) => {
38
- // Strip an opening single quote (the SQL string-literal case) before
39
- // resolving the path. The tokenizer keeps it as part of the word.
40
- let raw = currentWord;
41
- let hadOpeningSingleQuote = false;
42
- if (raw.startsWith("'")) {
43
- hadOpeningSingleQuote = true;
44
- raw = raw.slice(1);
45
- }
46
- // Split into dir + basename prefix. A trailing `/` means "enumerate this
47
- // dir" and basename prefix is empty.
48
- const lastSlash = raw.lastIndexOf("/");
49
- const dirPart = lastSlash === -1 ? "" : raw.slice(0, lastSlash + 1);
50
- const basePrefix = lastSlash === -1 ? raw : raw.slice(lastSlash + 1);
51
- // Resolve the directory to scan. Empty `dirPart` → cwd; otherwise it's
52
- // taken relative to cwd (or absolute if starts with `/`).
53
- const scanDir = dirPart === ""
54
- ? cwd
55
- : dirPart.startsWith("/")
56
- ? dirPart
57
- : join(cwd, dirPart);
58
- let entries;
59
- try {
60
- entries = readdirSync(scanDir);
61
- }
62
- catch {
63
- return [];
64
- }
65
- // Filter by prefix. Filesystem matching is case-sensitive on Linux/macOS
66
- // (case-insensitive on macOS by default, but we mirror upstream readline's
67
- // behaviour which honours the OS's path semantics — case-sensitive on
68
- // POSIX, which is what the conformance suite runs on).
69
- const filtered = entries.filter((e) => e.startsWith(basePrefix));
70
- // Sort alphabetically so the listing is predictable.
71
- filtered.sort();
72
- // Build the candidates. Each is `dirPart + entry` (full path matching
73
- // what the user typed) plus optional trailing `/` for directories.
74
- const candidates = [];
75
- for (const entry of filtered) {
76
- let isDir = false;
77
- try {
78
- isDir = statSync(join(scanDir, entry)).isDirectory();
79
- }
80
- catch {
81
- // Broken symlink etc. — treat as regular file.
82
- }
83
- const full = dirPart + entry + (isDir ? "/" : "");
84
- candidates.push(full);
85
- }
86
- if (quoteCtx === "none") {
87
- // Bare paths. Preserve any opening single quote the user already typed
88
- // (rare for the no-quote contexts, but harmless to mirror).
89
- if (hadOpeningSingleQuote) {
90
- return candidates.map((c) => "'" + c);
91
- }
92
- return candidates;
93
- }
94
- // SQL string-literal context: wrap candidates in single quotes. The
95
- // line editor's `shouldAppendSpace` checks quote balance — unique
96
- // candidates close the quote (so `'...'` balances → trailing space
97
- // fires), multi-candidate common prefixes leave the closing quote off.
98
- if (candidates.length === 1 && !candidates[0].endsWith("/")) {
99
- // Unique file (not directory): close the quote so the trailing space
100
- // fires. Opening quote: re-add if user typed it, else add ourselves.
101
- return ["'" + candidates[0] + "'"];
102
- }
103
- // Multiple candidates OR directory match: opening quote only; closing
104
- // quote is deferred so the user keeps typing.
105
- return candidates.map((c) => "'" + c);
26
+ * Enumerate filesystem entries matching the partial path the user typed.
27
+ *
28
+ * `currentWord` is the raw token (post-tokenizer). It may contain a leading
29
+ * `'` (when the user is already inside a single-quoted SQL literal). The
30
+ * function strips that, looks up the dirname/basename, and returns full
31
+ * candidates that the line editor can splice in with `replaceLength = code
32
+ * points in currentWord`.
33
+ *
34
+ * Returns an empty array on any filesystem error (e.g. directory doesn't
35
+ * exist, no read permission). Tab completion is best-effort — failing to
36
+ * complete is the same as "no candidates".
37
+ */
38
+ const completeFilenames = (currentWord, quoteCtx, cwd = process.cwd()) => {
39
+ let raw = currentWord;
40
+ let hadOpeningSingleQuote = false;
41
+ if (raw.startsWith("'")) {
42
+ hadOpeningSingleQuote = true;
43
+ raw = raw.slice(1);
44
+ }
45
+ const lastSlash = raw.lastIndexOf("/");
46
+ const dirPart = lastSlash === -1 ? "" : raw.slice(0, lastSlash + 1);
47
+ const basePrefix = lastSlash === -1 ? raw : raw.slice(lastSlash + 1);
48
+ const scanDir = dirPart === "" ? cwd : dirPart.startsWith("/") ? dirPart : join(cwd, dirPart);
49
+ let entries;
50
+ try {
51
+ entries = readdirSync(scanDir);
52
+ } catch {
53
+ return [];
54
+ }
55
+ const filtered = entries.filter((e) => e.startsWith(basePrefix));
56
+ filtered.sort();
57
+ const candidates = [];
58
+ for (const entry of filtered) {
59
+ let isDir = false;
60
+ try {
61
+ isDir = statSync(join(scanDir, entry)).isDirectory();
62
+ } catch {}
63
+ const full = dirPart + entry + (isDir ? "/" : "");
64
+ candidates.push(full);
65
+ }
66
+ if (quoteCtx === "none") {
67
+ if (hadOpeningSingleQuote) return candidates.map((c) => "'" + c);
68
+ return candidates;
69
+ }
70
+ if (candidates.length === 1 && !candidates[0].endsWith("/")) return ["'" + candidates[0] + "'"];
71
+ return candidates.map((c) => "'" + c);
106
72
  };
107
73
  /**
108
- * Helper used by `rules.ts` to decide whether the SQL `COPY` we're
109
- * completing for is a `FROM` (input file) or `TO` (output file). Returns
110
- * `true` for either — both want filename completion.
111
- *
112
- * `prevWords` here is the full prev-words token list. We look for the
113
- * pattern `COPY <table>+ [FROM|TO]` anywhere as a tail match.
114
- */
115
- export const isCopyFromOrTo = (prevWords) => {
116
- if (prevWords.length < 3)
117
- return false;
118
- // Walk from the end backward: the immediate prev word must be FROM or TO,
119
- // and somewhere earlier must be COPY (case-insensitive).
120
- const last = prevWords[prevWords.length - 1].toUpperCase();
121
- if (last !== "FROM" && last !== "TO")
122
- return false;
123
- for (let i = prevWords.length - 2; i >= 0; i--) {
124
- if (prevWords[i].toUpperCase() === "COPY")
125
- return true;
126
- // If we walk past the start of statement (e.g. another keyword like
127
- // SELECT) we abort — only the SQL `COPY` form should match.
128
- if (prevWords[i].toUpperCase() === "SELECT" ||
129
- prevWords[i].toUpperCase() === "INSERT" ||
130
- prevWords[i].toUpperCase() === "UPDATE" ||
131
- prevWords[i].toUpperCase() === "DELETE" ||
132
- prevWords[i].toUpperCase() === "WITH") {
133
- return false;
134
- }
135
- }
136
- return false;
74
+ * Helper used by `rules.ts` to decide whether the SQL `COPY` we're
75
+ * completing for is a `FROM` (input file) or `TO` (output file). Returns
76
+ * `true` for either — both want filename completion.
77
+ *
78
+ * `prevWords` here is the full prev-words token list. We look for the
79
+ * pattern `COPY <table>+ [FROM|TO]` anywhere as a tail match.
80
+ */
81
+ const isCopyFromOrTo = (prevWords) => {
82
+ if (prevWords.length < 3) return false;
83
+ const last = prevWords[prevWords.length - 1].toUpperCase();
84
+ if (last !== "FROM" && last !== "TO") return false;
85
+ for (let i = prevWords.length - 2; i >= 0; i--) {
86
+ if (prevWords[i].toUpperCase() === "COPY") return true;
87
+ if (prevWords[i].toUpperCase() === "SELECT" || prevWords[i].toUpperCase() === "INSERT" || prevWords[i].toUpperCase() === "UPDATE" || prevWords[i].toUpperCase() === "DELETE" || prevWords[i].toUpperCase() === "WITH") return false;
88
+ }
89
+ return false;
90
+ };
91
+ const _internals = {
92
+ basename,
93
+ dirname
137
94
  };
138
- // Re-export for tests.
139
- export const _internals = { basename, dirname };
95
+ //#endregion
96
+ export { _internals, completeFilenames, isCopyFromOrTo };
@@ -1,104 +1,66 @@
1
- /**
2
- * Public entrypoint for psql tab completion.
3
- *
4
- * The `LineEditor` (WP-24) accepts a `Completer` of shape
5
- * `(input, cursor) => Promise<CompletionResult>`. We return a CURRIED
6
- * factory `psqlCompleter(ctx)` that captures the settings reference so the
7
- * completer can read `ctx.settings.db` lazily on every call — important
8
- * because `\c` swaps out the live connection.
9
- *
10
- * `CompletionResult` (defined in lineEditor/complete.ts):
11
- *
12
- * - candidates: string[] the list shown / inserted
13
- * - commonPrefix: string longest prefix the editor can insert without
14
- * making a choice (`apply` walks this)
15
- * - replaceLength: number code-points to chop off the buffer before
16
- * inserting `candidates[i]` / commonPrefix
17
- *
18
- * Our `findCompletions` returns raw candidate strings; we compute the common
19
- * prefix here. `replaceLength` is the code-point length of the partial word
20
- * the cursor was sitting on (`currentWord`), which we already compute in
21
- * `splitForCompletion` during tokenization.
22
- */
23
1
  import { splitForCompletion } from "./matcher.js";
24
2
  import { findCompletions } from "./rules.js";
3
+ //#region src/psql/complete/index.ts
25
4
  /**
26
- * Build a completer bound to the given settings. The settings reference is
27
- * captured by closure; we never snapshot, so changes to `settings.db` (via
28
- * `\c`) take effect immediately.
29
- */
30
- export const psqlCompleter = (ctx) => {
31
- const completer = async (input, cursor) => {
32
- const { prevWords, currentWord, replaceLength } = splitForCompletion(input, cursor);
33
- const ruleCtx = {
34
- settings: ctx.settings,
35
- queryBuf: ctx.getQueryBuf?.() ?? "",
36
- };
37
- const { candidates } = await findCompletions(prevWords, currentWord, ruleCtx);
38
- // De-duplicate while preserving order.
39
- const seen = new Set();
40
- const deduped = [];
41
- for (const c of candidates) {
42
- if (!seen.has(c)) {
43
- seen.add(c);
44
- deduped.push(c);
45
- }
46
- }
47
- // Stable sort so the listing is predictable. The first-character sort
48
- // key uses lowercase so case-mixed candidate lists don't look chaotic.
49
- deduped.sort((a, b) => {
50
- const la = a.toLowerCase();
51
- const lb = b.toLowerCase();
52
- if (la < lb)
53
- return -1;
54
- if (la > lb)
55
- return 1;
56
- return 0;
57
- });
58
- const commonPrefix = longestCommonPrefix(deduped, currentWord);
59
- return {
60
- candidates: deduped,
61
- commonPrefix,
62
- replaceLength,
63
- };
64
- };
65
- return completer;
5
+ * Build a completer bound to the given settings. The settings reference is
6
+ * captured by closure; we never snapshot, so changes to `settings.db` (via
7
+ * `\c`) take effect immediately.
8
+ */
9
+ const psqlCompleter = (ctx) => {
10
+ const completer = async (input, cursor) => {
11
+ const { prevWords, currentWord, replaceLength } = splitForCompletion(input, cursor);
12
+ const ruleCtx = {
13
+ settings: ctx.settings,
14
+ queryBuf: ctx.getQueryBuf?.() ?? ""
15
+ };
16
+ const { candidates } = await findCompletions(prevWords, currentWord, ruleCtx);
17
+ const seen = /* @__PURE__ */ new Set();
18
+ const deduped = [];
19
+ for (const c of candidates) if (!seen.has(c)) {
20
+ seen.add(c);
21
+ deduped.push(c);
22
+ }
23
+ deduped.sort((a, b) => {
24
+ const la = a.toLowerCase();
25
+ const lb = b.toLowerCase();
26
+ if (la < lb) return -1;
27
+ if (la > lb) return 1;
28
+ return 0;
29
+ });
30
+ return {
31
+ candidates: deduped,
32
+ commonPrefix: longestCommonPrefix(deduped, currentWord),
33
+ replaceLength
34
+ };
35
+ };
36
+ return completer;
66
37
  };
67
- // ---------------------------------------------------------------------------
68
- // Helpers.
69
- // ---------------------------------------------------------------------------
70
38
  /**
71
- * Return the longest common prefix of all candidates, but never shorter
72
- * than the user's partial input (so we don't backtrack the buffer when the
73
- * completion is a no-op).
74
- *
75
- * If there's a single candidate, returns it whole.
76
- */
39
+ * Return the longest common prefix of all candidates, but never shorter
40
+ * than the user's partial input (so we don't backtrack the buffer when the
41
+ * completion is a no-op).
42
+ *
43
+ * If there's a single candidate, returns it whole.
44
+ */
77
45
  const longestCommonPrefix = (candidates, fallback) => {
78
- if (candidates.length === 0)
79
- return fallback;
80
- if (candidates.length === 1)
81
- return candidates[0];
82
- // Use code-point iteration so multi-byte characters don't get cut.
83
- const arrs = candidates.map((c) => Array.from(c));
84
- const minLen = arrs.reduce((m, a) => Math.min(m, a.length), Infinity);
85
- const out = [];
86
- for (let i = 0; i < minLen; i++) {
87
- const first = arrs[0][i];
88
- let ok = true;
89
- for (let j = 1; j < arrs.length; j++) {
90
- if (arrs[j][i] !== first) {
91
- ok = false;
92
- break;
93
- }
94
- }
95
- if (!ok)
96
- break;
97
- out.push(first);
98
- }
99
- // Common prefix should not be shorter than what's already typed.
100
- const candidatePrefix = out.join("");
101
- if (candidatePrefix.length >= fallback.length)
102
- return candidatePrefix;
103
- return fallback;
46
+ if (candidates.length === 0) return fallback;
47
+ if (candidates.length === 1) return candidates[0];
48
+ const arrs = candidates.map((c) => Array.from(c));
49
+ const minLen = arrs.reduce((m, a) => Math.min(m, a.length), Infinity);
50
+ const out = [];
51
+ for (let i = 0; i < minLen; i++) {
52
+ const first = arrs[0][i];
53
+ let ok = true;
54
+ for (let j = 1; j < arrs.length; j++) if (arrs[j][i] !== first) {
55
+ ok = false;
56
+ break;
57
+ }
58
+ if (!ok) break;
59
+ out.push(first);
60
+ }
61
+ const candidatePrefix = out.join("");
62
+ if (candidatePrefix.length >= fallback.length) return candidatePrefix;
63
+ return fallback;
104
64
  };
65
+ //#endregion
66
+ export { psqlCompleter };