neon 3.0.0 → 3.1.0

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