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
package/dist/dev/env.js CHANGED
@@ -1,446 +1,365 @@
1
- import { createNeonApiFromOptions, loadConfigFromFile, } from "@neon/config";
2
- import { plan, pullConfig } from "@neon/config-runtime";
3
- import { NEON_ENV_VAR_KEYS } from "../_shared/env-core/env.js";
4
- import { fetchEnvReusingSecrets, } from "../_shared/env-core/reuse-secrets.js";
5
1
  import { log } from "../log.js";
6
2
  import { getCliName } from "../utils/cli_name.js";
3
+ import { NEON_ENV_VAR_KEYS } from "../_shared/env-core/env.js";
4
+ import { formatInstallCommand, resolvePackageManager } from "../utils/package_manager.js";
5
+ import { fetchEnvReusingSecrets } from "../_shared/env-core/reuse-secrets.js";
6
+ import { createNeonApiFromOptions, loadConfigFromFile } from "@neon/config";
7
+ import { plan, pullConfig } from "@neon/config-runtime";
8
+ //#region src/dev/env.ts
7
9
  /** The API-targeting options every runtime call forwards from the context. */
8
10
  const apiOptions = (ctx) => ({
9
- ...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
10
- ...(ctx.apiHost ? { apiHost: ctx.apiHost } : {}),
11
- ...(ctx.api ? { api: ctx.api } : {}),
11
+ ...ctx.apiKey ? { apiKey: ctx.apiKey } : {},
12
+ ...ctx.apiHost ? { apiHost: ctx.apiHost } : {},
13
+ ...ctx.api ? { api: ctx.api } : {}
12
14
  });
13
15
  /**
14
- * Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
15
- * Data API, a bucket, the AI Gateway) that the linked remote branch does not
16
- * have yet. Unlike every other failure in {@link resolveDevEnv} — which degrades
17
- * to "run without injection" — this is a hard stop: the user's intent (a policy)
18
- * cannot be honored, and silently dropping the secret would be more confusing
19
- * than refusing to start. The fix is to provision the resource first.
20
- */
21
- export class DevEnvMismatchError extends Error {
22
- constructor() {
23
- super(...arguments);
24
- this.name = "DevEnvMismatchError";
25
- }
26
- }
16
+ * Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
17
+ * Data API, a bucket, the AI Gateway) that the linked remote branch does not
18
+ * have yet. Unlike every other failure in {@link resolveDevEnv} — which degrades
19
+ * to "run without injection" — this is a hard stop: the user's intent (a policy)
20
+ * cannot be honored, and silently dropping the secret would be more confusing
21
+ * than refusing to start. The fix is to provision the resource first.
22
+ */
23
+ var DevEnvMismatchError = class extends Error {
24
+ constructor(..._args) {
25
+ super(..._args);
26
+ this.name = "DevEnvMismatchError";
27
+ }
28
+ };
27
29
  /**
28
- * Signals that no project/branch context could be resolved, so there is nothing to
29
- * resolve env from. `resolveDevEnv` degrades on this (dev runs without injection);
30
- * `env pull` surfaces it (an explicit pull needs a branch).
31
- */
32
- export class MissingBranchContextError extends Error {
33
- constructor() {
34
- super(...arguments);
35
- this.name = "MissingBranchContextError";
36
- }
37
- }
30
+ * Signals that no project/branch context could be resolved, so there is nothing to
31
+ * resolve env from. `resolveDevEnv` degrades on this (dev runs without injection);
32
+ * `env pull` surfaces it (an explicit pull needs a branch).
33
+ */
34
+ var MissingBranchContextError = class extends Error {
35
+ constructor(..._args2) {
36
+ super(..._args2);
37
+ this.name = "MissingBranchContextError";
38
+ }
39
+ };
38
40
  /**
39
- * Thrown when an explicit `--service` selection names a service the branch does not have.
40
- * Unlike the policy path — where the same situation is a {@link DevEnvMismatchError} pointing
41
- * at `deploy` — the user named the service on the command line, so the fix is to provision it
42
- * or drop it from the selection.
43
- */
44
- export class ServiceNotOnBranchError extends Error {
45
- constructor() {
46
- super(...arguments);
47
- this.name = "ServiceNotOnBranchError";
48
- }
49
- }
41
+ * Thrown when an explicit `--service` selection names a service the branch does not have.
42
+ * Unlike the policy path — where the same situation is a {@link DevEnvMismatchError} pointing
43
+ * at `deploy` — the user named the service on the command line, so the fix is to provision it
44
+ * or drop it from the selection.
45
+ */
46
+ var ServiceNotOnBranchError = class extends Error {
47
+ constructor(..._args3) {
48
+ super(..._args3);
49
+ this.name = "ServiceNotOnBranchError";
50
+ }
51
+ };
50
52
  /**
51
- * Resolve the branch's Neon env vars (pooled / direct `DATABASE_URL`, plus Auth /
52
- * Data API when enabled) into a `{ KEY: value }` map. Shared by `neon dev` (which
53
- * injects them) and `neon env pull` (which writes them to a `.env` file).
54
- *
55
- * Tiered:
56
- *
57
- * 0. {@link DevEnvContext.services} is set -> that selection *is* the policy, and any
58
- * `neon.ts` is ignored. See {@link resolveSelectedServices}.
59
- * 1. a `neon.ts` policy is found -> the policy is the source of truth. We first
60
- * check it against the branch's live state (`plan`); if it declares a resource
61
- * the branch is missing, we stop with a {@link DevEnvMismatchError} pointing at
62
- * `neonctl deploy`. Otherwise `fetchEnv` evaluates the policy.
63
- * 2. no `neon.ts`, but a project + branch are known -> `pullConfig` reads the
64
- * branch's live state (Auth / Data API enablement plus any object-storage
65
- * buckets) into a config, then `fetchEnv` resolves what is actually enabled —
66
- * so a branch with a bucket gets its `AWS_*` storage vars pulled with no policy.
67
- * With {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
68
- * `pullConfig` cannot read it back.
69
- * 3. otherwise -> throw {@link MissingBranchContextError}.
70
- *
71
- * Unlike {@link resolveDevEnv}, this never swallows errors — callers decide how to
72
- * handle them.
73
- */
74
- export const resolveNeonEnvVars = async (ctx) => {
75
- if (ctx.services) {
76
- return await resolveSelectedServices(ctx, ctx.services);
77
- }
78
- const config = await loadNeonConfig(ctx.cwd);
79
- if (config) {
80
- if (!ctx.projectId || !ctx.branchId) {
81
- throw new MissingBranchContextError("Found a neon.ts but could not resolve the project/branch. " +
82
- `Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass ` +
83
- "--project-id / --branch.");
84
- }
85
- // Resolve env from the policy with its `preview.functions` removed. Functions carry no
86
- // branch-level secrets — their env comes from the local `neon.ts` `functions.<slug>.env`,
87
- // layered per-function by the dev server — so env resolution never needs the functions
88
- // API. Probing it (via `plan`/`fetchEnv`) only adds a failure mode: an undeployed
89
- // function, or a project where the Functions Preview isn't enabled, would error and sink
90
- // ALL injection (including DATABASE_URL). Stripping functions keeps env resolution honest
91
- // while leaving buckets / AI Gateway / Auth / Data API fully checked — those DO carry
92
- // secrets, so a declared-but-missing one still hard-stops (see assertPolicyMatchesBranch).
93
- const envConfig = withoutPreviewFunctions(config);
94
- await assertPolicyMatchesBranch(envConfig, ctx);
95
- return await fetchAndProject(envConfig, ctx);
96
- }
97
- if (ctx.projectId && ctx.branchId) {
98
- const pulled = await pullConfig({
99
- projectId: ctx.projectId,
100
- branchId: ctx.branchId,
101
- ...apiOptions(ctx),
102
- });
103
- // `pulled.config` is already a `Config` (static auth/dataApi toggles, any
104
- // object-storage `preview.buckets`, and a branch tuning closure), so it feeds
105
- // straight into fetchEnv — no wrapping needed. pullConfig excludes functions and
106
- // the AI Gateway (neither can be faithfully read back), so fetchEnv never probes
107
- // the functions API here and only mints a storage credential when a bucket exists.
108
- if (!ctx.implyAiGateway) {
109
- return await fetchAndProject(pulled.config, ctx);
110
- }
111
- return await resolveWithImpliedGateway(pulled.config, ctx, {
112
- projectId: ctx.projectId,
113
- branchId: ctx.branchId,
114
- });
115
- }
116
- throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / ` +
117
- `\`${getCliName()} checkout\`) or pass --project-id and --branch.`);
53
+ * Resolve the branch's Neon env vars (pooled / direct `DATABASE_URL`, plus Auth /
54
+ * Data API when enabled) into a `{ KEY: value }` map. Shared by `neon dev` (which
55
+ * injects them) and `neon env pull` (which writes them to a `.env` file).
56
+ *
57
+ * Tiered:
58
+ *
59
+ * 0. {@link DevEnvContext.services} is set -> that selection *is* the policy, and any
60
+ * `neon.ts` is ignored. See {@link resolveSelectedServices}.
61
+ * 1. a `neon.ts` policy is found -> the policy is the source of truth. We first
62
+ * check it against the branch's live state (`plan`); if it declares a resource
63
+ * the branch is missing, we stop with a {@link DevEnvMismatchError} pointing at
64
+ * `neonctl deploy`. Otherwise `fetchEnv` evaluates the policy.
65
+ * 2. no `neon.ts`, but a project + branch are known -> `pullConfig` reads the
66
+ * branch's live state (Auth / Data API enablement plus any object-storage
67
+ * buckets) into a config, then `fetchEnv` resolves what is actually enabled —
68
+ * so a branch with a bucket gets its `AWS_*` storage vars pulled with no policy.
69
+ * With {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
70
+ * `pullConfig` cannot read it back.
71
+ * 3. otherwise -> throw {@link MissingBranchContextError}.
72
+ *
73
+ * Unlike {@link resolveDevEnv}, this never swallows errors — callers decide how to
74
+ * handle them.
75
+ */
76
+ const resolveNeonEnvVars = async (ctx) => {
77
+ if (ctx.services) return await resolveSelectedServices(ctx, ctx.services);
78
+ const config = await loadNeonConfig(ctx.cwd);
79
+ if (config) {
80
+ if (!ctx.projectId || !ctx.branchId) throw new MissingBranchContextError(`Found a neon.ts but could not resolve the project/branch. Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass --project-id / --branch.`);
81
+ const envConfig = withoutPreviewFunctions(config);
82
+ await assertPolicyMatchesBranch(envConfig, ctx);
83
+ return await fetchAndProject(envConfig, ctx);
84
+ }
85
+ if (ctx.projectId && ctx.branchId) {
86
+ const pulled = await pullConfig({
87
+ projectId: ctx.projectId,
88
+ branchId: ctx.branchId,
89
+ ...apiOptions(ctx)
90
+ });
91
+ if (!ctx.implyAiGateway) return await fetchAndProject(pulled.config, ctx);
92
+ return await resolveWithImpliedGateway(pulled.config, ctx, {
93
+ projectId: ctx.projectId,
94
+ branchId: ctx.branchId
95
+ });
96
+ }
97
+ throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / \`${getCliName()} checkout\`) or pass --project-id and --branch.`);
118
98
  };
119
99
  /** The same config with the AI Gateway enabled, leaving any other `preview` entries intact. */
120
100
  const withAiGateway = (config) => ({
121
- ...config,
122
- preview: { ...config.preview, aiGateway: true },
101
+ ...config,
102
+ preview: {
103
+ ...config.preview,
104
+ aiGateway: true
105
+ }
123
106
  });
124
107
  /**
125
- * Tier-2 resolution with the AI Gateway added on top of the branch's read-back state.
126
- *
127
- * The gateway is not detectable — `pullConfig` reports no enabled flag for it — so it is
128
- * implied rather than observed. Nobody named it, so it must never be the reason the whole
129
- * resolve fails: a project outside the regions where branch credentials exist would otherwise
130
- * lose its `DATABASE_URL` too, and `neon dev` would start with no env at all.
131
- *
132
- * So the gateway is only added once its credential endpoint has been shown to answer, by
133
- * reading the branch's credentials first. A project that does not have them says so on a
134
- * read, before anything is minted — which is the whole question, since the gateway's env is a
135
- * credential and nothing else.
136
- *
137
- * Deciding this **before** resolving, rather than by catching and retrying, is what keeps it
138
- * honest. A retry re-runs every call the first attempt made, so it would blame the gateway for
139
- * a one-off failure in shared work, and — worse — a first attempt that minted a credential and
140
- * then failed would be papered over by a second that succeeds without one, swallowing the
141
- * error and stranding a secret nobody holds. Once the read succeeds, a later failure is a real
142
- * failure and propagates: the same thing already happens on a branch with object storage,
143
- * whose credential is minted whether or not the gateway is involved.
144
- */
145
- const resolveWithImpliedGateway = async (config, ctx,
146
- /** Resolved by the caller, which is the branch this env belongs to. */
147
- branch) => {
148
- const unreachable = await credentialsUnreachable(ctx, branch);
149
- if (unreachable === null) {
150
- return await fetchAndProject(withAiGateway(config), ctx);
151
- }
152
- // Deliberately does not assert that the project lacks the gateway: a read can also fail
153
- // for a reason that has nothing to do with the feature, and this is not the place to
154
- // guess which. Name both, and the command that answers it.
155
- log.warning("Could not reach the AI Gateway's credentials, so %s were not resolved. Everything " +
156
- "else was. Either this project does not have the AI Gateway, or the call failed — " +
157
- `\`${getCliName()} env pull -s ai-gateway\` will say which.\nDetails: %s`, [
158
- NEON_ENV_VAR_KEYS.aiGateway.apiKey,
159
- NEON_ENV_VAR_KEYS.aiGateway.baseUrl,
160
- ].join(" and "), unreachable);
161
- return {
162
- ...(await fetchAndProject(config, ctx)),
163
- skipped: ["ai-gateway"],
164
- };
108
+ * Tier-2 resolution with the AI Gateway added on top of the branch's read-back state.
109
+ *
110
+ * The gateway is not detectable — `pullConfig` reports no enabled flag for it — so it is
111
+ * implied rather than observed. Nobody named it, so it must never be the reason the whole
112
+ * resolve fails: a project outside the regions where branch credentials exist would otherwise
113
+ * lose its `DATABASE_URL` too, and `neon dev` would start with no env at all.
114
+ *
115
+ * So the gateway is only added once its credential endpoint has been shown to answer, by
116
+ * reading the branch's credentials first. A project that does not have them says so on a
117
+ * read, before anything is minted — which is the whole question, since the gateway's env is a
118
+ * credential and nothing else.
119
+ *
120
+ * Deciding this **before** resolving, rather than by catching and retrying, is what keeps it
121
+ * honest. A retry re-runs every call the first attempt made, so it would blame the gateway for
122
+ * a one-off failure in shared work, and — worse — a first attempt that minted a credential and
123
+ * then failed would be papered over by a second that succeeds without one, swallowing the
124
+ * error and stranding a secret nobody holds. Once the read succeeds, a later failure is a real
125
+ * failure and propagates: the same thing already happens on a branch with object storage,
126
+ * whose credential is minted whether or not the gateway is involved.
127
+ */
128
+ const resolveWithImpliedGateway = async (config, ctx, branch) => {
129
+ const unreachable = await credentialsUnreachable(ctx, branch);
130
+ if (unreachable === null) return await fetchAndProject(withAiGateway(config), ctx);
131
+ log.warning(`Could not reach the AI Gateway's credentials, so %s were not resolved. Everything else was. Either this project does not have the AI Gateway, or the call failed — \`${getCliName()} env pull -s ai-gateway\` will say which.\nDetails: %s`, [NEON_ENV_VAR_KEYS.aiGateway.apiKey, NEON_ENV_VAR_KEYS.aiGateway.baseUrl].join(" and "), unreachable);
132
+ return {
133
+ ...await fetchAndProject(config, ctx),
134
+ skipped: ["ai-gateway"]
135
+ };
165
136
  };
166
137
  /**
167
- * Why the branch's credentials could not be read, or `null` when they could. A plain read: it
168
- * mints nothing, revokes nothing, and changes nothing, so asking is free of the side effects
169
- * that make a failed resolve ambiguous.
170
- */
138
+ * Why the branch's credentials could not be read, or `null` when they could. A plain read: it
139
+ * mints nothing, revokes nothing, and changes nothing, so asking is free of the side effects
140
+ * that make a failed resolve ambiguous.
141
+ */
171
142
  const credentialsUnreachable = async (ctx, branch) => {
172
- try {
173
- await apiFor(ctx).listCredentials(branch.projectId, branch.branchId);
174
- return null;
175
- }
176
- catch (err) {
177
- return err instanceof Error ? err.message : String(err);
178
- }
143
+ try {
144
+ await apiFor(ctx).listCredentials(branch.projectId, branch.branchId);
145
+ return null;
146
+ } catch (err) {
147
+ return err instanceof Error ? err.message : String(err);
148
+ }
179
149
  };
180
150
  /** The adapter for direct branch reads: the injected one in tests, else built from options. */
181
- const apiFor = (ctx) => ctx.api ??
182
- createNeonApiFromOptions("neon env", {
183
- ...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
184
- ...(ctx.apiHost ? { apiHost: ctx.apiHost } : {}),
185
- });
151
+ const apiFor = (ctx) => ctx.api ?? createNeonApiFromOptions("neon env", {
152
+ ...ctx.apiKey ? { apiKey: ctx.apiKey } : {},
153
+ ...ctx.apiHost ? { apiHost: ctx.apiHost } : {}
154
+ });
186
155
  /**
187
- * Tier-0: resolve exactly the services `--service` named, with `neon.ts` out of the picture.
188
- *
189
- * The selection is checked against the branch's live state so a service that is named but not
190
- * provisioned fails by name, instead of quietly contributing no vars. `postgres` and the AI
191
- * Gateway are not checked: every branch has Postgres, and the gateway has no branch-level
192
- * state to check (an unavailable one surfaces when its credential is minted).
193
- */
156
+ * Tier-0: resolve exactly the services `--service` named, with `neon.ts` out of the picture.
157
+ *
158
+ * The selection is checked against the branch's live state so a service that is named but not
159
+ * provisioned fails by name, instead of quietly contributing no vars. `postgres` and the AI
160
+ * Gateway are not checked: every branch has Postgres, and the gateway has no branch-level
161
+ * state to check (an unavailable one surfaces when its credential is minted).
162
+ */
194
163
  const resolveSelectedServices = async (ctx, services) => {
195
- const { projectId, branchId } = ctx;
196
- if (!projectId || !branchId) {
197
- throw new MissingBranchContextError("--service needs a project and branch to read from. " +
198
- `Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass ` +
199
- "--project-id / --branch.");
200
- }
201
- // Read only the services that were named, rather than going through `pullConfig`. That
202
- // keeps a selection independent of everything else on the branch — `pullConfig` also
203
- // enumerates functions and credentials, so a failure there would abort `-s auth` — and it
204
- // keeps an "object storage isn't available for this project" error intact, which
205
- // `pullConfig` degrades to an empty bucket list and would report as "no buckets".
206
- const api = apiFor(ctx);
207
- const has = (service) => services.includes(service);
208
- const [auth, dataApiEnabled, buckets] = await Promise.all([
209
- has("auth") ? api.getNeonAuth(projectId, branchId) : null,
210
- has("data-api") ? readDataApiEnabled(api, projectId, branchId) : null,
211
- has("object-storage")
212
- ? api.listBranchBuckets(projectId, branchId)
213
- : null,
214
- ]);
215
- const config = configForServices(services, branchId, {
216
- authEnabled: auth !== null,
217
- dataApiEnabled,
218
- buckets: buckets ?? [],
219
- });
220
- // A selection resolves part of the branch, so it must not revoke: the credential its
221
- // persisted secrets name may also back a service it is not resolving. See
222
- // `fetchEnvReusingSecrets`'s `revokeSuperseded`.
223
- return await fetchAndProject(config, ctx, { revokeSuperseded: false });
164
+ const { projectId, branchId } = ctx;
165
+ if (!projectId || !branchId) throw new MissingBranchContextError(`--service needs a project and branch to read from. Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass --project-id / --branch.`);
166
+ const api = apiFor(ctx);
167
+ const has = (service) => services.includes(service);
168
+ const [auth, dataApiEnabled, buckets] = await Promise.all([
169
+ has("auth") ? api.getNeonAuth(projectId, branchId) : null,
170
+ has("data-api") ? readDataApiEnabled(api, projectId, branchId) : null,
171
+ has("object-storage") ? api.listBranchBuckets(projectId, branchId) : null
172
+ ]);
173
+ const config = configForServices(services, branchId, {
174
+ authEnabled: auth !== null,
175
+ dataApiEnabled,
176
+ buckets: buckets ?? []
177
+ });
178
+ return await fetchAndProject(config, ctx, { revokeSuperseded: false });
224
179
  };
225
180
  /**
226
- * Whether the branch has a Data API integration — or `null` when that cannot be determined.
227
- *
228
- * It is enabled per branch *and database*, so this has to probe the database `fetchEnv` will
229
- * resolve the URL from, or the two would disagree. That is Neon's default `neondb`, else the
230
- * only database; several databases with no `neondb` is a case `fetchEnv` refuses to auto-pick
231
- * at all. Reporting "no Data API integration" there would be a claim this read cannot support,
232
- * so it answers `null` and lets `fetchEnv` raise its own ambiguity error, which names the
233
- * databases and the fix.
234
- */
181
+ * Whether the branch has a Data API integration — or `null` when that cannot be determined.
182
+ *
183
+ * It is enabled per branch *and database*, so this has to probe the database `fetchEnv` will
184
+ * resolve the URL from, or the two would disagree. That is Neon's default `neondb`, else the
185
+ * only database; several databases with no `neondb` is a case `fetchEnv` refuses to auto-pick
186
+ * at all. Reporting "no Data API integration" there would be a claim this read cannot support,
187
+ * so it answers `null` and lets `fetchEnv` raise its own ambiguity error, which names the
188
+ * databases and the fix.
189
+ */
235
190
  const readDataApiEnabled = async (api, projectId, branchId) => {
236
- const databases = await api.listBranchDatabases(projectId, branchId);
237
- const database = databases.find((db) => db.name === NEON_DEFAULT_DATABASE) ??
238
- (databases.length === 1 ? databases[0] : undefined);
239
- if (!database)
240
- return databases.length === 0 ? false : null;
241
- const dataApi = await api.getNeonDataApi(projectId, branchId, database.name);
242
- return dataApi !== null;
191
+ const databases = await api.listBranchDatabases(projectId, branchId);
192
+ const database = databases.find((db) => db.name === NEON_DEFAULT_DATABASE) ?? (databases.length === 1 ? databases[0] : void 0);
193
+ if (!database) return databases.length === 0 ? false : null;
194
+ return await api.getNeonDataApi(projectId, branchId, database.name) !== null;
243
195
  };
244
196
  /** Neon's default database, and the one `fetchEnv` prefers when a branch has several. */
245
197
  const NEON_DEFAULT_DATABASE = "neondb";
246
198
  /**
247
- * Build the `Config` an explicit `--service` selection stands for, raising
248
- * {@link ServiceNotOnBranchError} for anything the branch does not have. Naming a service
249
- * that isn't there has to fail rather than contribute no vars, or a scoped pull would report
250
- * "no Neon env variables to pull" — which reads as a statement about the branch rather than
251
- * about the selection.
252
- */
199
+ * Build the `Config` an explicit `--service` selection stands for, raising
200
+ * {@link ServiceNotOnBranchError} for anything the branch does not have. Naming a service
201
+ * that isn't there has to fail rather than contribute no vars, or a scoped pull would report
202
+ * "no Neon env variables to pull" — which reads as a statement about the branch rather than
203
+ * about the selection.
204
+ */
253
205
  const configForServices = (services, branchId, branch) => {
254
- // The command that provisions each one, for a user who may well have no `neon.ts` — in
255
- // which case `deploy` / `config apply` would be no help at all.
256
- const provisionWith = {
257
- auth: `${getCliName()} neon-auth enable`,
258
- "data-api": `${getCliName()} data-api create`,
259
- "object-storage": `${getCliName()} buckets create <name>`,
260
- };
261
- const notOnBranch = (service, what) => {
262
- throw new ServiceNotOnBranchError(`--service ${service}: branch ${branchId} has no ${what}, so there are no ` +
263
- `${service} env vars to pull. Provision it first (\`${provisionWith[service]}\`, ` +
264
- `or in the Neon Console), or drop ${service} from --service.`);
265
- };
266
- const config = {};
267
- if (services.includes("auth")) {
268
- if (!branch.authEnabled)
269
- notOnBranch("auth", "Neon Auth integration");
270
- config.auth = true;
271
- }
272
- if (services.includes("data-api")) {
273
- // Only a positive "not there" is an error; an undecidable read defers to `fetchEnv`.
274
- if (branch.dataApiEnabled === false) {
275
- notOnBranch("data-api", "Data API integration");
276
- }
277
- config.dataApi = true;
278
- }
279
- const preview = {};
280
- if (services.includes("object-storage")) {
281
- if (branch.buckets.length === 0) {
282
- notOnBranch("object-storage", "object-storage buckets");
283
- }
284
- preview.buckets = Object.fromEntries(branch.buckets.map((bucket) => [
285
- bucket.name,
286
- { access: bucket.accessLevel },
287
- ]));
288
- }
289
- if (services.includes("ai-gateway"))
290
- preview.aiGateway = true;
291
- if (Object.keys(preview).length > 0)
292
- config.preview = preview;
293
- return config;
206
+ const provisionWith = {
207
+ auth: `${getCliName()} neon-auth enable`,
208
+ "data-api": `${getCliName()} data-api create`,
209
+ "object-storage": `${getCliName()} buckets create <name>`
210
+ };
211
+ const notOnBranch = (service, what) => {
212
+ throw new ServiceNotOnBranchError(`--service ${service}: branch ${branchId} has no ${what}, so there are no ${service} env vars to pull. Provision it first (\`${provisionWith[service]}\`, or in the Neon Console), or drop ${service} from --service.`);
213
+ };
214
+ const config = {};
215
+ if (services.includes("auth")) {
216
+ if (!branch.authEnabled) notOnBranch("auth", "Neon Auth integration");
217
+ config.auth = true;
218
+ }
219
+ if (services.includes("data-api")) {
220
+ if (branch.dataApiEnabled === false) notOnBranch("data-api", "Data API integration");
221
+ config.dataApi = true;
222
+ }
223
+ const preview = {};
224
+ if (services.includes("object-storage")) {
225
+ if (branch.buckets.length === 0) notOnBranch("object-storage", "object-storage buckets");
226
+ preview.buckets = Object.fromEntries(branch.buckets.map((bucket) => [bucket.name, { access: bucket.accessLevel }]));
227
+ }
228
+ if (services.includes("ai-gateway")) preview.aiGateway = true;
229
+ if (Object.keys(preview).length > 0) config.preview = preview;
230
+ return config;
294
231
  };
295
232
  /**
296
- * `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
297
- *
298
- * - Success → `{ vars }` (possibly just the always-present Postgres URLs).
299
- * - No linked branch / project → `{ vars: {}, skipped }` with a "link a branch" hint; the
300
- * function still runs locally, just without Neon env.
301
- * - Any other failure (offline, transient API error) → `{ vars: {}, skipped }` naming the
302
- * cause; again non-fatal.
303
- * - {@link DevEnvMismatchError} (policy declares a secret-bearing service the branch lacks)
304
- * is the one hard stop and is re-thrown for the caller to surface.
305
- */
306
- export const resolveDevEnv = async (ctx) => {
307
- try {
308
- const { vars, credential } = await resolveNeonEnvVars(ctx);
309
- return { vars, credential };
310
- }
311
- catch (err) {
312
- if (err instanceof DevEnvMismatchError)
313
- throw err;
314
- if (err instanceof MissingBranchContextError) {
315
- log.debug("dev: %s; skipping env injection", err.message);
316
- return {
317
- vars: {},
318
- skipped: {
319
- reason: `no linked Neon branch — run \`${getCliName()} link\`, then ` +
320
- `\`${getCliName()} checkout <branch>\`, to inject DATABASE_URL and friends`,
321
- },
322
- };
323
- }
324
- const detail = err instanceof Error ? err.message : String(err);
325
- log.debug("dev: env resolution failed: %s", detail);
326
- return {
327
- vars: {},
328
- skipped: {
329
- reason: `could not reach Neon (${detail}); running without Neon env`,
330
- },
331
- };
332
- }
233
+ * `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
234
+ *
235
+ * - Success → `{ vars }` (possibly just the always-present Postgres URLs).
236
+ * - No linked branch / project → `{ vars: {}, skipped }` with a "link a branch" hint; the
237
+ * function still runs locally, just without Neon env.
238
+ * - Any other failure (offline, transient API error) → `{ vars: {}, skipped }` naming the
239
+ * cause; again non-fatal.
240
+ * - {@link DevEnvMismatchError} (policy declares a secret-bearing service the branch lacks)
241
+ * is the one hard stop and is re-thrown for the caller to surface.
242
+ */
243
+ const resolveDevEnv = async (ctx) => {
244
+ try {
245
+ const { vars, credential } = await resolveNeonEnvVars(ctx);
246
+ return {
247
+ vars,
248
+ credential
249
+ };
250
+ } catch (err) {
251
+ if (err instanceof DevEnvMismatchError) throw err;
252
+ if (err instanceof MissingBranchContextError) {
253
+ log.debug("dev: %s; skipping env injection", err.message);
254
+ return {
255
+ vars: {},
256
+ skipped: { reason: `no linked Neon branch — run \`${getCliName()} link\`, then \`${getCliName()} checkout <branch>\`, to inject DATABASE_URL and friends` }
257
+ };
258
+ }
259
+ const detail = err instanceof Error ? err.message : String(err);
260
+ log.debug("dev: env resolution failed: %s", detail);
261
+ return {
262
+ vars: {},
263
+ skipped: { reason: `could not reach Neon (${detail}); running without Neon env` }
264
+ };
265
+ }
333
266
  };
334
267
  /**
335
- * Return the policy with its `preview.functions` removed, so the env path never enumerates
336
- * functions against the Neon API. Functions are local-source-bundled and produce no
337
- * branch-level secrets, so they are irrelevant to env resolution; probing them only risks
338
- * failing the whole resolve (undeployed function, or Functions Preview disabled on the
339
- * project). Buckets / AI Gateway and the top-level Auth / Data API toggles are preserved —
340
- * they DO carry env, so they must still be checked and resolved. Returns the config
341
- * unchanged when it declares no functions.
342
- */
268
+ * Return the policy with its `preview.functions` removed, so the env path never enumerates
269
+ * functions against the Neon API. Functions are local-source-bundled and produce no
270
+ * branch-level secrets, so they are irrelevant to env resolution; probing them only risks
271
+ * failing the whole resolve (undeployed function, or Functions Preview disabled on the
272
+ * project). Buckets / AI Gateway and the top-level Auth / Data API toggles are preserved —
273
+ * they DO carry env, so they must still be checked and resolved. Returns the config
274
+ * unchanged when it declares no functions.
275
+ */
343
276
  const withoutPreviewFunctions = (config) => {
344
- const preview = config.preview;
345
- if (!preview?.functions)
346
- return config;
347
- const previewWithoutFunctions = { ...preview };
348
- delete previewWithoutFunctions.functions;
349
- return { ...config, preview: previewWithoutFunctions };
277
+ const preview = config.preview;
278
+ if (!preview?.functions) return config;
279
+ const previewWithoutFunctions = { ...preview };
280
+ delete previewWithoutFunctions.functions;
281
+ return {
282
+ ...config,
283
+ preview: previewWithoutFunctions
284
+ };
350
285
  };
351
286
  /**
352
- * Tier-1 guard. Dry-run the policy against the branch's live state and stop if
353
- * it declares a branch-level resource the branch is missing. Built on `plan` so
354
- * it covers every present and future provisionable resource for free: any
355
- * `create` action is a resource `neonctl deploy` would provision.
356
- *
357
- * Called with functions already stripped (see {@link withoutPreviewFunctions}), so the
358
- * `plan` probe never enumerates the functions API — an undeployed function, or a project
359
- * without the Functions Preview, must never block local dev or sink env injection.
360
- */
287
+ * Tier-1 guard. Dry-run the policy against the branch's live state and stop if
288
+ * it declares a branch-level resource the branch is missing. Built on `plan` so
289
+ * it covers every present and future provisionable resource for free: any
290
+ * `create` action is a resource `neonctl deploy` would provision.
291
+ *
292
+ * Called with functions already stripped (see {@link withoutPreviewFunctions}), so the
293
+ * `plan` probe never enumerates the functions API — an undeployed function, or a project
294
+ * without the Functions Preview, must never block local dev or sink env injection.
295
+ */
361
296
  const assertPolicyMatchesBranch = async (config, ctx) => {
362
- const result = await plan(config, {
363
- projectId: ctx.projectId,
364
- branchId: ctx.branchId,
365
- ...apiOptions(ctx),
366
- });
367
- const missing = result.applied.filter(isMissingResource);
368
- if (missing.length === 0)
369
- return;
370
- const names = missing.map((change) => change.identifier).join(", ");
371
- throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
372
- "does not have it yet, so the matching env vars cannot be injected. " +
373
- `Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), ` +
374
- `then re-run \`${getCliName()} dev\`.`);
297
+ const missing = (await plan(config, {
298
+ projectId: ctx.projectId,
299
+ branchId: ctx.branchId,
300
+ ...apiOptions(ctx)
301
+ })).applied.filter(isMissingResource);
302
+ if (missing.length === 0) return;
303
+ throw new DevEnvMismatchError(`Your neon.ts declares ${missing.map((change) => change.identifier).join(", ")} for branch ${ctx.branchId}, but the branch does not have it yet, so the matching env vars cannot be injected. Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), then re-run \`${getCliName()} dev\`.`);
375
304
  };
376
305
  /**
377
- * A planned change that provisions a branch-level resource the branch lacks: a
378
- * `create` on a service (Neon Auth, Data API, a bucket, the AI Gateway). Branch
379
- * setting drift (`update`) and `noop`s are ignored — they don't block local dev
380
- * — and functions are excluded (see {@link assertPolicyMatchesBranch}).
381
- */
382
- const isMissingResource = (change) => change.kind === "service" &&
383
- change.action === "create" &&
384
- !change.identifier.startsWith("function:");
306
+ * A planned change that provisions a branch-level resource the branch lacks: a
307
+ * `create` on a service (Neon Auth, Data API, a bucket, the AI Gateway). Branch
308
+ * setting drift (`update`) and `noop`s are ignored — they don't block local dev
309
+ * — and functions are excluded (see {@link assertPolicyMatchesBranch}).
310
+ */
311
+ const isMissingResource = (change) => change.kind === "service" && change.action === "create" && !change.identifier.startsWith("function:");
385
312
  const fetchAndProject = async (config, ctx, opts = {}) => fetchEnvReusingSecrets(config, {
386
- projectId: ctx.projectId,
387
- branch: ctx.branchId,
388
- ...apiOptions(ctx),
389
- ...(ctx.env ? { env: ctx.env } : {}),
390
- ...(opts.revokeSuperseded === false ? { revokeSuperseded: false } : {}),
313
+ projectId: ctx.projectId,
314
+ branch: ctx.branchId,
315
+ ...apiOptions(ctx),
316
+ ...ctx.env ? { env: ctx.env } : {},
317
+ ...opts.revokeSuperseded === false ? { revokeSuperseded: false } : {}
391
318
  });
392
319
  /**
393
- * Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
394
- * root. Returns `null` when there is none (the common "no config" case), and
395
- * surfaces real load errors (e.g. a syntax error in an existing file).
396
- */
320
+ * Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
321
+ * root. Returns `null` when there is none (the common "no config" case), and
322
+ * surfaces real load errors (e.g. a syntax error in an existing file).
323
+ */
397
324
  /**
398
- * Substrings that mark a module-resolution failure while loading `neon.ts` —
399
- * almost always because the project's dependencies aren't installed yet (the
400
- * config imports `@neon/config` & friends). Deliberately specific:
401
- * the generic "…or a missing dependency…" hint the loader always appends is
402
- * NOT in here, so a real syntax/runtime error doesn't get mislabeled.
403
- */
325
+ * Substrings that mark a module-resolution failure while loading `neon.ts` —
326
+ * almost always because the project's dependencies aren't installed yet (the
327
+ * config imports `@neon/config` & friends). Deliberately specific:
328
+ * the generic "…or a missing dependency…" hint the loader always appends is
329
+ * NOT in here, so a real syntax/runtime error doesn't get mislabeled.
330
+ */
404
331
  const MISSING_DEPENDENCY_HINTS = [
405
- "cannot find module",
406
- "cannot find package",
407
- "err_module_not_found",
408
- "failed to resolve",
409
- "could not resolve",
410
- "module not found",
332
+ "cannot find module",
333
+ "cannot find package",
334
+ "err_module_not_found",
335
+ "failed to resolve",
336
+ "could not resolve",
337
+ "module not found"
411
338
  ];
412
339
  /** Flatten an error and its `cause` chain to one lowercased string for matching. */
413
340
  const errorChainText = (err) => {
414
- const parts = [];
415
- let current = err;
416
- for (let depth = 0; current instanceof Error && depth < 6; depth++) {
417
- parts.push(current.message);
418
- current = current.cause;
419
- }
420
- return parts.join("\n").toLowerCase();
341
+ const parts = [];
342
+ let current = err;
343
+ for (let depth = 0; current instanceof Error && depth < 6; depth++) {
344
+ parts.push(current.message);
345
+ current = current.cause;
346
+ }
347
+ return parts.join("\n").toLowerCase();
421
348
  };
422
349
  const looksLikeMissingDependency = (err) => {
423
- const text = errorChainText(err);
424
- return MISSING_DEPENDENCY_HINTS.some((hint) => text.includes(hint));
350
+ const text = errorChainText(err);
351
+ return MISSING_DEPENDENCY_HINTS.some((hint) => text.includes(hint));
425
352
  };
426
353
  const loadNeonConfig = async (cwd) => {
427
- try {
428
- const { config } = await loadConfigFromFile({ cwd });
429
- return config;
430
- }
431
- catch (err) {
432
- const message = err instanceof Error ? err.message : String(err);
433
- if (/Could not find a Neon config file/i.test(message)) {
434
- return null;
435
- }
436
- // A neon.ts that imports a package which isn't installed fails here with a
437
- // cryptic "Cannot find module …". Turn that into the actionable thing to do.
438
- if (looksLikeMissingDependency(err)) {
439
- throw new Error("Could not load neon.ts: a package it imports is not installed. " +
440
- "Did you run `npm install`? Install your dependencies " +
441
- "(npm / pnpm / yarn / bun), then try again.\n" +
442
- `Original error: ${message}`);
443
- }
444
- throw err;
445
- }
354
+ try {
355
+ const { config } = await loadConfigFromFile({ cwd });
356
+ return config;
357
+ } catch (err) {
358
+ const message = err instanceof Error ? err.message : String(err);
359
+ if (/Could not find a Neon config file/i.test(message)) return null;
360
+ if (looksLikeMissingDependency(err)) throw new Error(`Could not load neon.ts: a package it imports is not installed. Run \`${formatInstallCommand(resolvePackageManager(cwd))}\`, then try again.\nOriginal error: ${message}`);
361
+ throw err;
362
+ }
446
363
  };
364
+ //#endregion
365
+ export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, resolveDevEnv, resolveNeonEnvVars };