neon 2.47.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 +395 -0
  5. package/dist/_shared/env-core/reuse-secrets.js +179 -0
  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 +575 -658
  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 +642 -681
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -341
  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 -64
  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 +133 -147
  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 +9 -7
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 };