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
package/dist/context.js CHANGED
@@ -1,268 +1,216 @@
1
+ import { log } from "./log.js";
1
2
  import { accessSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
3
  import { dirname, normalize, resolve } from "node:path";
4
- import { log } from "./log.js";
4
+ import { homedir } from "node:os";
5
+ //#region src/context.ts
5
6
  /**
6
- * The branch pinned in a context, reading the current `branch` field and
7
- * falling back to the legacy `branchId` so pre-migration `.neon` files keep
8
- * working.
9
- */
10
- export const contextBranch = (context) => context.branch ?? context.branchId;
7
+ * The branch pinned in a context, reading the current `branch` field and
8
+ * falling back to the legacy `branchId` so pre-migration `.neon` files keep
9
+ * working.
10
+ */
11
+ const contextBranch = (context) => context.branch ?? context.branchId;
11
12
  /**
12
- * True when the invocation is the offline "current branch" probe:
13
- * `(config) status --current-branch`. This mode only reads the pinned branch
14
- * from the local `.neon` file (for shell prompts like starship), so it MUST
15
- * NOT touch the network — several middlewares (auth, analytics, single-project
16
- * resolution) consult this to early-return and skip their API calls / login.
17
- *
18
- * Gated on the exact command as well as the flag so an accidental
19
- * `--current-branch` on an unrelated command (e.g. `config plan`, where the flag
20
- * is undefined but non-strict yargs still parses it) can't silently skip
21
- * auth/analytics. The probe is only `status` (the top-level alias) or
22
- * `config status` (`_ = ['config', 'status']`).
23
- */
24
- export const isCurrentBranchProbe = (args) => args.currentBranch === true &&
25
- (args._[0] === "status" ||
26
- (args._[0] === "config" && args._[1] === "status"));
13
+ * True when the invocation is the offline "current branch" probe:
14
+ * `(config) status --current-branch`. This mode only reads the pinned branch
15
+ * from the local `.neon` file (for shell prompts like starship), so it MUST
16
+ * NOT touch the network — several middlewares (auth, analytics, single-project
17
+ * resolution) consult this to early-return and skip their API calls / login.
18
+ *
19
+ * Gated on the exact command as well as the flag so an accidental
20
+ * `--current-branch` on an unrelated command (e.g. `config plan`, where the flag
21
+ * is undefined but non-strict yargs still parses it) can't silently skip
22
+ * auth/analytics. The probe is only `status` (the top-level alias) or
23
+ * `config status` (`_ = ['config', 'status']`).
24
+ */
25
+ const isCurrentBranchProbe = (args) => args.currentBranch === true && (args._[0] === "status" || args._[0] === "config" && args._[1] === "status");
27
26
  /**
28
- * `config init` only scaffolds a local `neon.ts` and installs npm packages — it
29
- * never calls the Neon API. Gated on the exact command path so the global auth
30
- * middleware and the single-project resolver can skip it (it runs with no API
31
- * client), mirroring {@link isCurrentBranchProbe}.
32
- *
33
- * `--from-branch` is the exception: it seeds the policy from a branch's live state, so it
34
- * needs both credentials and a resolved project. The raw argv is checked alongside the parsed
35
- * flag because this runs from middleware that executes before validation, where the parsed
36
- * value may not be populated yet (the same reason `analytics.ts` scans argv for
37
- * `--current-branch`).
38
- */
39
- export const isConfigInit = (args) => args._[0] === "config" &&
40
- args._[1] === "init" &&
41
- args.fromBranch !== true &&
42
- !process.argv.includes("--from-branch");
27
+ * `config init` only scaffolds a local `neon.ts` and installs npm packages — it
28
+ * never calls the Neon API. Gated on the exact command path so the global auth
29
+ * middleware and the single-project resolver can skip it (it runs with no API
30
+ * client), mirroring {@link isCurrentBranchProbe}.
31
+ *
32
+ * `--from-branch` is the exception: it seeds the policy from a branch's live state, so it
33
+ * needs both credentials and a resolved project. The raw argv is checked alongside the parsed
34
+ * flag because this runs from middleware that executes before validation, where the parsed
35
+ * value may not be populated yet (the same reason `analytics.ts` scans argv for
36
+ * `--current-branch`).
37
+ */
38
+ const isConfigInit = (args) => args._[0] === "config" && args._[1] === "init" && args.fromBranch !== true && !process.argv.includes("--from-branch");
43
39
  /**
44
- * `neon profile …` manages credentials on disk and never calls the Neon API, so the global
45
- * auth middleware must skip it — mirroring {@link isConfigInit}.
46
- *
47
- * More than a nicety: without this, listing your profiles would launch a browser login, and
48
- * removing a broken profile would demand you sign into it first. Removing a profile whose
49
- * access has already lapsed is the main reason to remove one.
50
- */
51
- export const isProfileCommand = (args) => args._[0] === "profile" || args._[0] === "profiles";
40
+ * `neon profile …` manages credentials on disk and never calls the Neon API, so the global
41
+ * auth middleware must skip it — mirroring {@link isConfigInit}.
42
+ *
43
+ * More than a nicety: without this, listing your profiles would launch a browser login, and
44
+ * removing a broken profile would demand you sign into it first. Removing a profile whose
45
+ * access has already lapsed is the main reason to remove one.
46
+ */
47
+ const isProfileCommand = (args) => args._[0] === "profile" || args._[0] === "profiles";
52
48
  /**
53
- * `neon api-keys …`, under either spelling. Exempts the group from context enrichment: how
54
- * far a credential reaches must come from an explicit flag, never from `.neon`.
55
- */
56
- export const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "api-key";
49
+ * `neon api-keys …`, under either spelling. Exempts the group from context enrichment: how
50
+ * far a credential reaches must come from an explicit flag, never from `.neon`.
51
+ */
52
+ const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "api-key";
57
53
  const CONTEXT_FILE = ".neon";
58
54
  const GITIGNORE_FILE = ".gitignore";
59
55
  const canAccessFile = (file) => {
60
- try {
61
- accessSync(file);
62
- return true;
63
- }
64
- catch {
65
- return false;
66
- }
56
+ try {
57
+ accessSync(file);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
67
62
  };
68
63
  /**
69
- * Walk upward to find an existing `.neon` file.
70
- *
71
- * The walk keeps the established home and POSIX-root boundaries, then also
72
- * stops when resolving a parent makes no progress. That second guard handles
73
- * Windows drive and UNC-share roots, whose paths do not equal `normalize("/")`.
74
- */
75
- export const walkContextFile = (cwd, root, home, resolvePath, canAccess) => {
76
- let currentDir = cwd;
77
- while (currentDir !== root && currentDir !== home) {
78
- const contextFile = resolvePath(currentDir, CONTEXT_FILE);
79
- if (canAccess(contextFile)) {
80
- return contextFile;
81
- }
82
- const parentDir = resolvePath(currentDir, "..");
83
- if (parentDir === currentDir) {
84
- break;
85
- }
86
- currentDir = parentDir;
87
- }
88
- return resolvePath(cwd, CONTEXT_FILE);
64
+ * Walk upward to find an existing `.neon` file.
65
+ *
66
+ * The walk keeps the established home and POSIX-root boundaries, then also
67
+ * stops when resolving a parent makes no progress. That second guard handles
68
+ * Windows drive and UNC-share roots, whose paths do not equal `normalize("/")`.
69
+ */
70
+ const walkContextFile = (cwd, root, home, resolvePath, canAccess) => {
71
+ let currentDir = cwd;
72
+ while (currentDir !== root && currentDir !== home) {
73
+ const contextFile = resolvePath(currentDir, CONTEXT_FILE);
74
+ if (canAccess(contextFile)) return contextFile;
75
+ const parentDir = resolvePath(currentDir, "..");
76
+ if (parentDir === currentDir) break;
77
+ currentDir = parentDir;
78
+ }
79
+ return resolvePath(cwd, CONTEXT_FILE);
89
80
  };
90
81
  /**
91
- * Resolve the default `.neon` path for the current working directory.
92
- *
93
- * Walks UP from `cwd` looking ONLY for an already-existing `.neon` file so
94
- * commands run from a sub-directory of a linked project still pick up the
95
- * project's context. If no `.neon` is found, the path defaults to
96
- * `<cwd>/.neon`, which makes `neonctl link` and `neonctl set-context`
97
- * predictable: they always write the context file into the directory they
98
- * were invoked from.
99
- *
100
- * Historically the walk also considered `package.json` and `.git` as project
101
- * markers, but that led to surprising behaviour when running `link` from a
102
- * fresh sub-directory inside an unrelated repo (the new link would land in
103
- * the parent repo's root instead of the cwd).
104
- *
105
- * `cwd` is overridable so tests can exercise the walk-up without mutating
106
- * `process.cwd()` (which would race with other tests running in parallel).
107
- */
108
- export const currentContextFile = (cwd = process.cwd()) => walkContextFile(cwd, normalize("/"), homedir(), resolve, canAccessFile);
109
- export const readContextFile = (file) => {
110
- try {
111
- return JSON.parse(readFileSync(file, "utf-8"));
112
- }
113
- catch {
114
- return {};
115
- }
82
+ * Resolve the default `.neon` path for the current working directory.
83
+ *
84
+ * Walks UP from `cwd` looking ONLY for an already-existing `.neon` file so
85
+ * commands run from a sub-directory of a linked project still pick up the
86
+ * project's context. If no `.neon` is found, the path defaults to
87
+ * `<cwd>/.neon`, which makes `neonctl link` and `neonctl set-context`
88
+ * predictable: they always write the context file into the directory they
89
+ * were invoked from.
90
+ *
91
+ * Historically the walk also considered `package.json` and `.git` as project
92
+ * markers, but that led to surprising behaviour when running `link` from a
93
+ * fresh sub-directory inside an unrelated repo (the new link would land in
94
+ * the parent repo's root instead of the cwd).
95
+ *
96
+ * `cwd` is overridable so tests can exercise the walk-up without mutating
97
+ * `process.cwd()` (which would race with other tests running in parallel).
98
+ */
99
+ const currentContextFile = (cwd = process.cwd()) => walkContextFile(cwd, normalize("/"), homedir(), resolve, canAccessFile);
100
+ const readContextFile = (file) => {
101
+ try {
102
+ return JSON.parse(readFileSync(file, "utf-8"));
103
+ } catch {
104
+ return {};
105
+ }
116
106
  };
117
- export const enrichFromContext = (args) => {
118
- // `link` and the deprecated `set-context` manage the context file themselves
119
- // and must see the raw flags rather than values pre-filled from an existing
120
- // `.neon`, so skip enrichment for both.
121
- if (args._[0] === "link" || args._[0] === "set-context") {
122
- return;
123
- }
124
- // `api-keys` mints credentials, and how far a credential reaches must be something the
125
- // user typed — never something inherited from whichever project happens to be checked
126
- // out. Enriched here, `api-keys create --name ci` in a linked directory would quietly
127
- // produce a key scoped to that project instead of the account key it asked for.
128
- if (isApiKeysCommand(args)) {
129
- return;
130
- }
131
- // `profile create --mint` mints one too, and for the same reason must take its scope only
132
- // from what was typed: enriched here, running it inside a linked directory would quietly
133
- // produce a key scoped to that project rather than the account or organization asked for.
134
- // No `profile` subcommand has any use for a project or branch.
135
- if (isProfileCommand(args)) {
136
- return;
137
- }
138
- const context = readContextFile(args.contextFile);
139
- if (!args.orgId) {
140
- args.orgId = context.orgId;
141
- }
142
- if (!args.projectId) {
143
- args.projectId = context.projectId;
144
- }
145
- if (!args.branch &&
146
- !args.id &&
147
- !args.name &&
148
- context.projectId === args.projectId) {
149
- args.branch = contextBranch(context);
150
- }
107
+ const enrichFromContext = (args) => {
108
+ if (args._[0] === "link" || args._[0] === "set-context") return;
109
+ if (isApiKeysCommand(args)) return;
110
+ if (isProfileCommand(args)) return;
111
+ const context = readContextFile(args.contextFile);
112
+ if (!args.orgId) args.orgId = context.orgId;
113
+ if (!args.projectId) args.projectId = context.projectId;
114
+ if (!args.branch && !args.id && !args.name && context.projectId === args.projectId) args.branch = contextBranch(context);
151
115
  };
152
- export const updateContextFile = (file, context) => {
153
- writeFileSync(file, JSON.stringify(context, null, 2));
116
+ const updateContextFile = (file, context) => {
117
+ writeFileSync(file, JSON.stringify(context, null, 2));
154
118
  };
155
119
  /**
156
- * Shared primitive used by `link`, the deprecated `set-context`, and `checkout`
157
- * to persist context. Mirrors the destructive write semantics of
158
- * `updateContextFile` — any field not present in `context` is dropped from the
159
- * file.
160
- *
161
- * `.gitignore` scaffolding only happens when the context file is being
162
- * *created* (it didn't exist before this write). On updates to an existing
163
- * `.neon` we never touch `.gitignore`, so a user who deliberately un-ignored
164
- * the file (e.g. to commit shared context) won't have the entry re-added on
165
- * every subsequent command.
166
- */
167
- export const applyContext = (file, context) => {
168
- const isNewFile = !existsSync(file);
169
- updateContextFile(file, context);
170
- if (isNewFile) {
171
- ensureGitignored(file);
172
- }
120
+ * Shared primitive used by `link`, the deprecated `set-context`, and `checkout`
121
+ * to persist context. Mirrors the destructive write semantics of
122
+ * `updateContextFile` — any field not present in `context` is dropped from the
123
+ * file.
124
+ *
125
+ * `.gitignore` scaffolding only happens when the context file is being
126
+ * *created* (it didn't exist before this write). On updates to an existing
127
+ * `.neon` we never touch `.gitignore`, so a user who deliberately un-ignored
128
+ * the file (e.g. to commit shared context) won't have the entry re-added on
129
+ * every subsequent command.
130
+ */
131
+ const applyContext = (file, context) => {
132
+ const isNewFile = !existsSync(file);
133
+ updateContextFile(file, context);
134
+ if (isNewFile) ensureGitignored(file);
173
135
  };
174
136
  /**
175
- * Low-level writer for callers that already hold the resolved identifiers and
176
- * just need to record them — e.g. `init` or `projects create`, which create a
177
- * project and want to link it without the resolution, verification, prompting,
178
- * or env-pull that `link` performs.
179
- *
180
- * Unlike the loose {@link applyContext}, this enforces at the type level that
181
- * `orgId` and `projectId` are present, so the `.neon` file never ends up with a
182
- * dangling project that has no org. The branch stays optional. It writes through
183
- * {@link applyContext}, so the same `.gitignore` scaffolding applies.
184
- */
185
- export const setContext = (file, context) => {
186
- applyContext(file, {
187
- orgId: context.orgId,
188
- projectId: context.projectId,
189
- branch: context.branch,
190
- });
137
+ * Low-level writer for callers that already hold the resolved identifiers and
138
+ * just need to record them — e.g. `init` or `projects create`, which create a
139
+ * project and want to link it without the resolution, verification, prompting,
140
+ * or env-pull that `link` performs.
141
+ *
142
+ * Unlike the loose {@link applyContext}, this enforces at the type level that
143
+ * `orgId` and `projectId` are present, so the `.neon` file never ends up with a
144
+ * dangling project that has no org. The branch stays optional. It writes through
145
+ * {@link applyContext}, so the same `.gitignore` scaffolding applies.
146
+ */
147
+ const setContext = (file, context) => {
148
+ applyContext(file, {
149
+ orgId: context.orgId,
150
+ projectId: context.projectId,
151
+ branch: context.branch
152
+ });
191
153
  };
192
154
  /**
193
- * Make sure the `.gitignore` next to `file` covers the file's basename — used for the `.neon`
194
- * context file and for a `.env` we create (both carry credentials that must not be committed).
195
- * Creates the `.gitignore` if it doesn't exist, otherwise appends the entry only when nothing
196
- * there already covers it: an exact line, or a basename glob such as `.env*` / `*.local`
197
- * (see {@link gitignoreCovers}), so a repo that already ignores env files doesn't collect a
198
- * redundant line per pull.
199
- *
200
- * Best-effort: a failure here (e.g. read-only filesystem) is logged at debug
201
- * level and swallowed; persisting the context file is the primary goal and
202
- * must not be blocked by a `.gitignore` write error.
203
- */
204
- export const ensureGitignored = (file) => {
205
- try {
206
- const dir = dirname(file);
207
- const entry = basenameOf(file);
208
- const gitignorePath = resolve(dir, GITIGNORE_FILE);
209
- if (!existsSync(gitignorePath)) {
210
- writeFileSync(gitignorePath, `${entry}\n`);
211
- return;
212
- }
213
- const current = readFileSync(gitignorePath, "utf-8");
214
- if (hasGitignoreEntry(current, entry)) {
215
- return;
216
- }
217
- const needsLeadingNewline = current.length > 0 && !current.endsWith("\n");
218
- const addition = `${needsLeadingNewline ? "\n" : ""}${entry}\n`;
219
- writeFileSync(gitignorePath, current + addition);
220
- }
221
- catch (err) {
222
- const message = err instanceof Error ? err.message : String(err);
223
- log.debug("Failed to update .gitignore next to %s: %s", file, message);
224
- }
155
+ * Make sure the `.gitignore` next to `file` covers the file's basename — used for the `.neon`
156
+ * context file and for a `.env` we create (both carry credentials that must not be committed).
157
+ * Creates the `.gitignore` if it doesn't exist, otherwise appends the entry only when nothing
158
+ * there already covers it: an exact line, or a basename glob such as `.env*` / `*.local`
159
+ * (see {@link gitignoreCovers}), so a repo that already ignores env files doesn't collect a
160
+ * redundant line per pull.
161
+ *
162
+ * Best-effort: a failure here (e.g. read-only filesystem) is logged at debug
163
+ * level and swallowed; persisting the context file is the primary goal and
164
+ * must not be blocked by a `.gitignore` write error.
165
+ */
166
+ const ensureGitignored = (file) => {
167
+ try {
168
+ const dir = dirname(file);
169
+ const entry = basenameOf(file);
170
+ const gitignorePath = resolve(dir, GITIGNORE_FILE);
171
+ if (!existsSync(gitignorePath)) {
172
+ writeFileSync(gitignorePath, `${entry}\n`);
173
+ return;
174
+ }
175
+ const current = readFileSync(gitignorePath, "utf-8");
176
+ if (hasGitignoreEntry(current, entry)) return;
177
+ const addition = `${current.length > 0 && !current.endsWith("\n") ? "\n" : ""}${entry}\n`;
178
+ writeFileSync(gitignorePath, current + addition);
179
+ } catch (err) {
180
+ const message = err instanceof Error ? err.message : String(err);
181
+ log.debug("Failed to update .gitignore next to %s: %s", file, message);
182
+ }
225
183
  };
226
184
  const basenameOf = (file) => {
227
- const parts = file.split(/[\\/]/);
228
- return parts[parts.length - 1] || CONTEXT_FILE;
185
+ const parts = file.split(/[\\/]/);
186
+ return parts[parts.length - 1] || CONTEXT_FILE;
229
187
  };
230
188
  const hasGitignoreEntry = (content, entry) => {
231
- return content
232
- .split(/\r?\n/)
233
- .some((line) => gitignoreCovers(line.trim(), entry));
189
+ return content.split(/\r?\n/).some((line) => gitignoreCovers(line.trim(), entry));
234
190
  };
235
191
  /**
236
- * Whether a single `.gitignore` line already ignores `entry` (a bare basename like `.neon` or
237
- * `.env.local`).
238
- *
239
- * Deliberately narrow: an exact match, or a glob **without a path separator** — the
240
- * `.env*` / `*.local` / `.env.?` shapes that repos actually use for env files — matched
241
- * against the whole basename. Everything else returns false, which at worst appends an entry
242
- * git already covers (harmless) rather than skipping one it doesn't (a committed credential).
243
- * That's why path-scoped patterns (`config/.env`), negations (`!.env.local`), character
244
- * classes, and comments are all treated as "does not cover".
245
- */
192
+ * Whether a single `.gitignore` line already ignores `entry` (a bare basename like `.neon` or
193
+ * `.env.local`).
194
+ *
195
+ * Deliberately narrow: an exact match, or a glob **without a path separator** — the
196
+ * `.env*` / `*.local` / `.env.?` shapes that repos actually use for env files — matched
197
+ * against the whole basename. Everything else returns false, which at worst appends an entry
198
+ * git already covers (harmless) rather than skipping one it doesn't (a committed credential).
199
+ * That's why path-scoped patterns (`config/.env`), negations (`!.env.local`), character
200
+ * classes, and comments are all treated as "does not cover".
201
+ */
246
202
  const gitignoreCovers = (line, entry) => {
247
- if (line === "" || line.startsWith("#") || line.startsWith("!")) {
248
- return false;
249
- }
250
- // A trailing slash marks a directory-only pattern; the leading one anchors to the
251
- // .gitignore's own directory, which is exactly where `entry` lives.
252
- const pattern = line.replace(/\/$/, "").replace(/^\//, "");
253
- if (pattern === entry)
254
- return true;
255
- if (pattern.includes("/") || pattern.includes("["))
256
- return false;
257
- if (!pattern.includes("*") && !pattern.includes("?"))
258
- return false;
259
- return globToRegExp(pattern).test(entry);
203
+ if (line === "" || line.startsWith("#") || line.startsWith("!")) return false;
204
+ const pattern = line.replace(/\/$/, "").replace(/^\//, "");
205
+ if (pattern === entry) return true;
206
+ if (pattern.includes("/") || pattern.includes("[")) return false;
207
+ if (!pattern.includes("*") && !pattern.includes("?")) return false;
208
+ return globToRegExp(pattern).test(entry);
260
209
  };
261
210
  /** Compile a separator-free `.gitignore` glob (`*` / `?` only) into an anchored RegExp. */
262
211
  const globToRegExp = (pattern) => {
263
- const source = pattern
264
- .replace(/[.+^${}()|\\]/g, "\\$&")
265
- .replace(/\*/g, "[^/]*")
266
- .replace(/\?/g, "[^/]");
267
- return new RegExp(`^${source}$`);
212
+ const source = pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
213
+ return new RegExp(`^${source}$`);
268
214
  };
215
+ //#endregion
216
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isConfigInit, isCurrentBranchProbe, isProfileCommand, readContextFile, setContext, updateContextFile, walkContextFile };
@@ -1,56 +1,47 @@
1
- import { contextBranch, currentContextFile, readContextFile, } from "./context.js";
2
1
  import { log } from "./log.js";
2
+ import { contextBranch, currentContextFile, readContextFile } from "./context.js";
3
3
  import { getCliName } from "./utils/cli_name.js";
4
+ //#region src/current_branch_fast_path.ts
4
5
  /**
5
- * Offline fast path for `(config) status --current-branch` (used by shell prompts).
6
- *
7
- * Reading the pinned branch out of the local `.neon` file does not need the CLI's
8
- * full command tree, `@neondatabase/api-client`, or yargs — importing those is ~200ms,
9
- * which dwarfs the actual work. So the entry point ({@link file://./cli.ts}) calls this
10
- * BEFORE importing `index.js`, and only falls through to the full CLI when this returns
11
- * `false`. On the fast path the process loads only this module + `context.js`/`log.js`
12
- * (~25ms total incl. Node startup) instead of ~230ms.
13
- *
14
- * It mirrors the `--current-branch` short-circuit in `status()` (commands/config.ts):
15
- * print the pinned branch to stdout and exit 0, or print nothing + a `neonctl checkout`
16
- * hint on stderr and exit non-zero when no branch is pinned.
17
- *
18
- * Deliberately conservative: only the EXACT bare invocation is handled —
19
- * `status --current-branch` or `config status --current-branch` with no other args.
20
- * Anything else (extra flags like `--context-file`/`--output`, more args, etc.) returns
21
- * `false` and flows through the normal yargs pipeline, so behavior can never diverge —
22
- * the worst case is "not faster", never "wrong".
23
- *
24
- * @returns `true` if it handled the invocation (caller should NOT load the full CLI).
25
- */
26
- export const tryCurrentBranchFastPath = (argv,
27
- // `cwd` is overridable so tests can exercise the `.neon` walk-up without mutating
28
- // `process.cwd()` (which isn't allowed in vitest workers), mirroring currentContextFile.
29
- cwd = process.cwd()) => {
30
- // argv is [execPath, scriptPath, ...userArgs].
31
- if (!isExactCurrentBranchInvocation(argv.slice(2))) {
32
- return false;
33
- }
34
- const branch = contextBranch(readContextFile(currentContextFile(cwd)));
35
- if (branch) {
36
- process.stdout.write(`${branch}\n`);
37
- }
38
- else {
39
- log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
40
- process.exitCode = 1;
41
- }
42
- return true;
6
+ * Offline fast path for `(config) status --current-branch` (used by shell prompts).
7
+ *
8
+ * Reading the pinned branch out of the local `.neon` file does not need the CLI's
9
+ * full command tree, `@neondatabase/api-client`, or yargs — importing those is ~200ms,
10
+ * which dwarfs the actual work. So the entry point ({@link file://./cli.ts}) calls this
11
+ * BEFORE importing `index.js`, and only falls through to the full CLI when this returns
12
+ * `false`. On the fast path the process loads only this module + `context.js`/`log.js`
13
+ * (~25ms total incl. Node startup) instead of ~230ms.
14
+ *
15
+ * It mirrors the `--current-branch` short-circuit in `status()` (commands/config.ts):
16
+ * print the pinned branch to stdout and exit 0, or print nothing + a `neonctl checkout`
17
+ * hint on stderr and exit non-zero when no branch is pinned.
18
+ *
19
+ * Deliberately conservative: only the EXACT bare invocation is handled —
20
+ * `status --current-branch` or `config status --current-branch` with no other args.
21
+ * Anything else (extra flags like `--context-file`/`--output`, more args, etc.) returns
22
+ * `false` and flows through the normal yargs pipeline, so behavior can never diverge —
23
+ * the worst case is "not faster", never "wrong".
24
+ *
25
+ * @returns `true` if it handled the invocation (caller should NOT load the full CLI).
26
+ */
27
+ const tryCurrentBranchFastPath = (argv, cwd = process.cwd()) => {
28
+ if (!isExactCurrentBranchInvocation(argv.slice(2))) return false;
29
+ const branch = contextBranch(readContextFile(currentContextFile(cwd)));
30
+ if (branch) process.stdout.write(`${branch}\n`);
31
+ else {
32
+ log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
33
+ process.exitCode = 1;
34
+ }
35
+ return true;
43
36
  };
44
37
  /**
45
- * True only for `status --current-branch` or `config status --current-branch` with no
46
- * other arguments. Any extra token (another flag, `--context-file`, `=`-style flags,
47
- * positional args) makes this false so the full CLI handles it.
48
- */
38
+ * True only for `status --current-branch` or `config status --current-branch` with no
39
+ * other arguments. Any extra token (another flag, `--context-file`, `=`-style flags,
40
+ * positional args) makes this false so the full CLI handles it.
41
+ */
49
42
  const isExactCurrentBranchInvocation = (args) => {
50
- const rest = args[0] === "status"
51
- ? args.slice(1)
52
- : args[0] === "config" && args[1] === "status"
53
- ? args.slice(2)
54
- : null;
55
- return rest !== null && rest.length === 1 && rest[0] === "--current-branch";
43
+ const rest = args[0] === "status" ? args.slice(1) : args[0] === "config" && args[1] === "status" ? args.slice(2) : null;
44
+ return rest !== null && rest.length === 1 && rest[0] === "--current-branch";
56
45
  };
46
+ //#endregion
47
+ export { tryCurrentBranchFastPath };