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
@@ -1,331 +1,271 @@
1
- import { existsSync } from "node:fs";
2
- import { NEON_ENV_VAR_KEYS } from "@neon/env";
3
- import chalk from "chalk";
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import { log } from "../log.js";
4
3
  import { ensureGitignored } from "../context.js";
5
- import { resolveNeonEnvVars } from "../dev/env.js";
4
+ import { getCliName } from "../utils/cli_name.js";
5
+ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
6
6
  import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
7
- import { ENV_PULL_SERVICES, ENV_PULL_UNAVAILABLE, envServiceKeys, ownedEnvServiceKeys, } from "../env_services.js";
8
- import { log } from "../log.js";
9
- import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption, } from "../neon_services.js";
7
+ import { NEON_ENV_VAR_KEYS } from "../_shared/env-core/env.js";
8
+ import { ENV_PULL_SERVICES, ENV_PULL_UNAVAILABLE, envServiceKeys, ownedEnvServiceKeys } from "../env_services.js";
9
+ import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
10
10
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
11
11
  import { announceTargetBranch } from "../utils/branch_notice.js";
12
- import { getCliName } from "../utils/cli_name.js";
13
- import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
14
- export const command = "env";
15
- export const describe = "Manage a branch's Neon env variables locally";
12
+ import { resolveNeonEnvVars } from "../dev/env.js";
13
+ import { existsSync } from "node:fs";
14
+ import chalk from "chalk";
15
+ //#region src/commands/env.ts
16
+ var env_exports = /* @__PURE__ */ __exportAll({
17
+ ENV_PULL_SKIPPED_HINT: () => ENV_PULL_SKIPPED_HINT,
18
+ autoPullEnvAfterPin: () => autoPullEnvAfterPin,
19
+ builder: () => builder,
20
+ command: () => "env",
21
+ describe: () => describe,
22
+ handler: () => handler,
23
+ pull: () => pull,
24
+ renderAgentPullNote: () => renderAgentPullNote
25
+ });
26
+ const command = "env";
27
+ const describe = "Manage a branch's Neon env variables locally";
16
28
  /**
17
- * Shown (to stderr) when `link` / `checkout` skip the bundled env pull because the user passed
18
- * `--no-env-pull`. Names the two ways to get the branch's vars without an on-disk file written
19
- * eagerly: an explicit `neonctl env pull`, or runtime injection via `neon-env run`.
20
- */
21
- export const ENV_PULL_SKIPPED_HINT = `Skipped env pull (--no-env-pull). Run \`${getCliName()} env pull\` to write this branch’s env vars ` +
22
- "(DATABASE_URL, …) into a local .env, or inject them at runtime with `neon-env run -- <your dev command>`.";
23
- export const builder = (argv) => argv
24
- .usage("$0 env <sub-command> [options]")
25
- .options({
26
- "project-id": { describe: "Project ID", type: "string" },
27
- branch: { describe: "Branch ID or name", type: "string" },
28
- })
29
- .middleware(fillSingleProject)
30
- .command("pull", "Write the branch's Neon env variables to a local .env file", (yargs) => yargs
31
- .usage("$0 env pull [options]")
32
- .options({
33
- file: {
34
- describe: "Target .env file to write. Defaults to an existing .env, " +
35
- "otherwise .env.local. Only Neon variables are updated; other " +
36
- "lines are preserved.",
37
- type: "string",
38
- },
39
- service: servicesOption({
40
- key: "service",
41
- allowed: ENV_PULL_SERVICES,
42
- describe: "Pull only these services' variables",
43
- also: "Overrides neon.ts, and prunes only within the services you name.",
44
- }),
45
- })
46
- .epilogue([
47
- "",
48
- "What gets pulled, in precedence order:",
49
- " 1. --service, when given — exactly those, ignoring neon.ts.",
50
- " 2. neon.ts, when this directory has one.",
51
- " 3. Otherwise everything the branch has, plus the AI Gateway —",
52
- " which mints a branch credential for it.",
53
- "",
54
- "The pull bundled into link / checkout / config apply follows 2 and 3",
55
- "without the AI Gateway, so it never mints a credential you did not ask",
56
- "for. Run `env pull` to add it.",
57
- ].join("\n"))
58
- .example("$0 env pull", "Write the linked branch's Neon vars into .env.local (or .env if present)")
59
- .example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file")
60
- .example("$0 env pull -s ai-gateway -s postgres", "Pull only the AI Gateway and Postgres variables"), async (args) => {
61
- const raw = servicesFlagValue(args.service);
62
- // Explicit `env pull` announces the branch it's reading from up front so the user
63
- // can catch "pulled env from the wrong branch" before it overwrites their .env. The
64
- // bundled auto-pull (link / checkout / apply) stays quiet — those already report the
65
- // branch they pinned/applied to.
66
- //
67
- // It also implies the AI Gateway when there is no neon.ts, so a bare `env pull`
68
- // really does write everything the branch can give you. The bundled auto-pull does
69
- // not: minting a credential for a service the user never named is not something a
70
- // side effect of `link` / `checkout` / `apply` should do.
71
- await pull({
72
- ...args,
73
- ...(raw
74
- ? {
75
- services: parseServices(raw, {
76
- allowed: ENV_PULL_SERVICES,
77
- whyUnavailable: ENV_PULL_UNAVAILABLE,
78
- flag: "--service",
79
- onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical)),
80
- }),
81
- }
82
- : {}),
83
- }, { announce: true, implyAiGateway: raw === undefined });
84
- })
85
- .demandCommand(1);
86
- export const handler = (args) => args;
29
+ * Shown (to stderr) when `link` / `checkout` skip the bundled env pull because the user passed
30
+ * `--no-env-pull`. Names the two ways to get the branch's vars without an on-disk file written
31
+ * eagerly: an explicit `neonctl env pull`, or runtime injection via `neon-env run`.
32
+ */
33
+ const ENV_PULL_SKIPPED_HINT = `Skipped env pull (--no-env-pull). Run \`${getCliName()} env pull\` to write this branch’s env vars (DATABASE_URL, …) into a local .env, or inject them at runtime with \`neon-env run -- <your dev command>\`.`;
34
+ const builder = (argv) => argv.usage("$0 env <sub-command> [options]").options({
35
+ "project-id": {
36
+ describe: "Project ID",
37
+ type: "string"
38
+ },
39
+ branch: {
40
+ describe: "Branch ID or name",
41
+ type: "string"
42
+ }
43
+ }).middleware(fillSingleProject).command("pull", "Write the branch's Neon env variables to a local .env file", (yargs) => yargs.usage("$0 env pull [options]").options({
44
+ file: {
45
+ describe: "Target .env file to write. Defaults to an existing .env, otherwise .env.local. Only Neon variables are updated; other lines are preserved.",
46
+ type: "string"
47
+ },
48
+ service: servicesOption({
49
+ key: "service",
50
+ allowed: ENV_PULL_SERVICES,
51
+ describe: "Pull only these services' variables",
52
+ also: "Overrides neon.ts, and prunes only within the services you name."
53
+ })
54
+ }).epilogue([
55
+ "",
56
+ "What gets pulled, in precedence order:",
57
+ " 1. --service, when given — exactly those, ignoring neon.ts.",
58
+ " 2. neon.ts, when this directory has one.",
59
+ " 3. Otherwise everything the branch has, plus the AI Gateway —",
60
+ " which mints a branch credential for it.",
61
+ "",
62
+ "The pull bundled into link / checkout / config apply follows 2 and 3",
63
+ "without the AI Gateway, so it never mints a credential you did not ask",
64
+ "for. Run `env pull` to add it."
65
+ ].join("\n")).example("$0 env pull", "Write the linked branch's Neon vars into .env.local (or .env if present)").example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file").example("$0 env pull -s ai-gateway -s postgres", "Pull only the AI Gateway and Postgres variables"), async (args) => {
66
+ const raw = servicesFlagValue(args.service);
67
+ await pull({
68
+ ...args,
69
+ ...raw ? { services: parseServices(raw, {
70
+ allowed: ENV_PULL_SERVICES,
71
+ whyUnavailable: ENV_PULL_UNAVAILABLE,
72
+ flag: "--service",
73
+ onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
74
+ }) } : {}
75
+ }, {
76
+ announce: true,
77
+ implyAiGateway: raw === void 0
78
+ });
79
+ }).demandCommand(1);
80
+ const handler = (args) => args;
87
81
  /** Every OS-level env var name `@neon/env` can emit, used only for reporting. */
88
82
  const NEON_VAR_NAMES = Object.values(NEON_ENV_VAR_KEYS).flatMap((group) => Object.values(group));
89
83
  /**
90
- * The Neon env vars `env pull` *owns*, so it removes any that the branch no longer has when
91
- * it reconciles the local `.env` (see {@link pull}). Scoped to the unambiguously Neon-named
92
- * vars — the `NEON_*` aliases plus `DATABASE_URL[_UNPOOLED]` — so switching a working
93
- * directory to a project/branch without Auth / the Data API drops the now-stale
94
- * `NEON_AUTH_*` / `NEON_DATA_API_*` lines instead of leaving credentials for features that
95
- * aren't enabled.
96
- *
97
- * Deliberately **excludes** the storage vars Neon projects onto third-party SDK names
98
- * (`AWS_*`): those collide with credentials a user may set by hand, so `env pull` only ever
99
- * writes them, never prunes them. The AI Gateway is emitted solely under its Neon-branded
100
- * vars (`NEON_AI_GATEWAY_*`), which are owned and pruned.
101
- */
84
+ * The Neon env vars `env pull` *owns*, so it removes any that the branch no longer has when
85
+ * it reconciles the local `.env` (see {@link pull}). Scoped to the unambiguously Neon-named
86
+ * vars — the `NEON_*` aliases plus `DATABASE_URL[_UNPOOLED]` — so switching a working
87
+ * directory to a project/branch without Auth / the Data API drops the now-stale
88
+ * `NEON_AUTH_*` / `NEON_DATA_API_*` lines instead of leaving credentials for features that
89
+ * aren't enabled.
90
+ *
91
+ * Deliberately **excludes** the storage vars Neon projects onto third-party SDK names
92
+ * (`AWS_*`): those collide with credentials a user may set by hand, so `env pull` only ever
93
+ * writes them, never prunes them. The AI Gateway is emitted solely under its Neon-branded
94
+ * vars (`NEON_AI_GATEWAY_*`), which are owned and pruned.
95
+ */
102
96
  const NEON_OWNED_ENV_KEYS = [
103
- ...Object.values(NEON_ENV_VAR_KEYS.postgres),
104
- ...Object.values(NEON_ENV_VAR_KEYS.auth),
105
- ...Object.values(NEON_ENV_VAR_KEYS.dataApi),
106
- ...Object.values(NEON_ENV_VAR_KEYS.aiGateway),
97
+ ...Object.values(NEON_ENV_VAR_KEYS.postgres),
98
+ ...Object.values(NEON_ENV_VAR_KEYS.auth),
99
+ ...Object.values(NEON_ENV_VAR_KEYS.dataApi),
100
+ ...Object.values(NEON_ENV_VAR_KEYS.aiGateway)
107
101
  ];
108
- export const pull = async (props, opts = {}) => {
109
- const cwd = props.cwd ?? process.cwd();
110
- const branch = await resolveBranchRef(props);
111
- if (opts.announce) {
112
- announceTargetBranch(props, branch, "Pulling env from branch");
113
- }
114
- const branchId = branch.branchId;
115
- // Resolve the target file first and layer its current contents under the resolver's env
116
- // source. This lets `fetchEnv` reuse one-time secrets that are already on disk — Neon Auth
117
- // keys and the unified branch credential's `api_token` / `s3_secret_access_key`, which the
118
- // API returns exactly once — instead of minting a fresh credential on every pull.
119
- const targetPath = resolveEnvFilePath(cwd, props.file);
120
- const fileExisted = existsSync(targetPath);
121
- const existingEnv = fileExisted ? readEnvFile(targetPath) : {};
122
- // Reuse `neon dev`'s tiered resolver (neon.ts policy -> plan gate -> fetchEnv, else
123
- // pullConfig -> fetchEnv). Unlike dev, an unresolved context or failure is surfaced —
124
- // `env pull` is an explicit action, so it should error rather than write nothing.
125
- const { vars, credential, skipped } = await resolveNeonEnvVars({
126
- cwd,
127
- projectId: props.projectId,
128
- branchId,
129
- env: { ...process.env, ...existingEnv },
130
- ...(props.services ? { services: props.services } : {}),
131
- ...(opts.implyAiGateway ? { implyAiGateway: true } : {}),
132
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
133
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
134
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
135
- });
136
- const neonVars = pickServiceVars(pickNeonVars(vars), props.services);
137
- if (Object.keys(neonVars).length === 0) {
138
- log.info("No Neon env variables to pull for this branch (no DATABASE_URL or " +
139
- "enabled Auth / Data API).");
140
- return { status: "empty" };
141
- }
142
- // Reconcile rather than blindly merge: write the branch's current Neon vars and prune any
143
- // Neon-owned vars the branch no longer has (e.g. NEON_AUTH_* / NEON_DATA_API_* carried over
144
- // from a previous project/branch). Non-Neon lines are always preserved.
145
- const { written, removed } = mergeEnvFile(targetPath, neonVars, {
146
- managedKeys: managedKeysFor(props.services, unreachedButCurrent(skipped, existingEnv, branchId)),
147
- });
148
- log.info("Pulled %d Neon variable%s into %s: %s", written.length, written.length === 1 ? "" : "s", targetPath, written.join(", "));
149
- if (removed.length > 0) {
150
- log.info("Removed %d stale Neon variable%s not enabled on this branch: %s", removed.length, removed.length === 1 ? "" : "s", removed.join(", "));
151
- }
152
- // A new credential means the values that back object storage / the AI Gateway just
153
- // changed, so anything else holding the old ones (a deployed preview, a second checkout)
154
- // needs the new values. Name the keys rather than leaving the user to diff the file.
155
- if (credential?.issued) {
156
- log.info("Issued a new branch credential — these now hold fresh values: %s", credential.keys.join(", "));
157
- if (credential.revoked.length > 0) {
158
- log.info("Revoked the credential it replaced (%s).", credential.revoked.join(", "));
159
- }
160
- else if (credential.superseded.length > 0) {
161
- // An unscoped pull revokes what it supersedes and says so above. A scoped one
162
- // cannot — it may not be the only service on that credential — so it leaves the
163
- // old one live. Say that too, rather than letting the identical-looking output
164
- // imply the branch is not accumulating credentials. Driven by what the resolver
165
- // actually declined to revoke, so a first pull (which supersedes nothing) does
166
- // not send the user hunting for a credential that was never there.
167
- log.info("Left the credential it replaced live (%s): a pull scoped with --service " +
168
- "can't tell which other services still use it. Revoke it in the Neon " +
169
- "Console if nothing does.", credential.superseded.join(", "));
170
- }
171
- }
172
- // A dotenv file *we* create holds live branch credentials (DATABASE_URL, Auth keys, service
173
- // tokens), so ignore it the same way the `.neon` context file is — otherwise a fresh repo is
174
- // one `git add -A` away from committing them. Only on creation: re-adding the entry on every
175
- // pull would fight a user who deliberately un-ignored a file they want to commit.
176
- if (!fileExisted) {
177
- ensureGitignored(targetPath);
178
- }
179
- // When the branch has the AI Gateway enabled, the pulled credentials always work, but
180
- // serving is plan-gated and the model set can be reduced on the beta — surface that as a
181
- // courtesy notice (best-effort; never fails the pull). The freshly pulled token lets us
182
- // probe the branch's own /v1/models to detect a reduced catalog.
183
- const gatewayBaseUrl = neonVars.NEON_AI_GATEWAY_BASE_URL;
184
- const gatewayToken = neonVars.NEON_AI_GATEWAY_TOKEN;
185
- if (gatewayBaseUrl && gatewayToken) {
186
- await warnAiGateway({
187
- apiClient: props.apiClient,
188
- projectId: props.projectId,
189
- branchId,
190
- gateway: { baseUrl: gatewayBaseUrl, token: gatewayToken },
191
- });
192
- }
193
- return {
194
- status: "written",
195
- written,
196
- file: targetPath,
197
- ...(credential && credential.keys.length > 0 ? { credential } : {}),
198
- ...(skipped && skipped.length > 0 ? { skipped } : {}),
199
- };
102
+ const pull = async (props, opts = {}) => {
103
+ const cwd = props.cwd ?? process.cwd();
104
+ const branch = await resolveBranchRef(props);
105
+ if (opts.announce) announceTargetBranch(props, branch, "Pulling env from branch");
106
+ const branchId = branch.branchId;
107
+ const targetPath = resolveEnvFilePath(cwd, props.file);
108
+ const fileExisted = existsSync(targetPath);
109
+ const existingEnv = fileExisted ? readEnvFile(targetPath) : {};
110
+ const { vars, credential, skipped } = await resolveNeonEnvVars({
111
+ cwd,
112
+ projectId: props.projectId,
113
+ branchId,
114
+ env: {
115
+ ...process.env,
116
+ ...existingEnv
117
+ },
118
+ ...props.services ? { services: props.services } : {},
119
+ ...opts.implyAiGateway ? { implyAiGateway: true } : {},
120
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
121
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
122
+ ...props.runtimeApi ? { api: props.runtimeApi } : {}
123
+ });
124
+ const neonVars = pickServiceVars(pickNeonVars(vars), props.services);
125
+ if (Object.keys(neonVars).length === 0) {
126
+ log.info("No Neon env variables to pull for this branch (no DATABASE_URL or enabled Auth / Data API).");
127
+ return { status: "empty" };
128
+ }
129
+ const { written, removed } = mergeEnvFile(targetPath, neonVars, { managedKeys: managedKeysFor(props.services, unreachedButCurrent(skipped, existingEnv, branchId)) });
130
+ log.info("Pulled %d Neon variable%s into %s: %s", written.length, written.length === 1 ? "" : "s", targetPath, written.join(", "));
131
+ if (removed.length > 0) log.info("Removed %d stale Neon variable%s not enabled on this branch: %s", removed.length, removed.length === 1 ? "" : "s", removed.join(", "));
132
+ if (credential?.issued) {
133
+ log.info("Issued a new branch credential — these now hold fresh values: %s", credential.keys.join(", "));
134
+ if (credential.revoked.length > 0) log.info("Revoked the credential it replaced (%s).", credential.revoked.join(", "));
135
+ else if (credential.superseded.length > 0) log.info("Left the credential it replaced live (%s): a pull scoped with --service can't tell which other services still use it. Revoke it in the Neon Console if nothing does.", credential.superseded.join(", "));
136
+ }
137
+ if (!fileExisted) ensureGitignored(targetPath);
138
+ const gatewayBaseUrl = neonVars.NEON_AI_GATEWAY_BASE_URL;
139
+ const gatewayToken = neonVars.NEON_AI_GATEWAY_TOKEN;
140
+ if (gatewayBaseUrl && gatewayToken) await warnAiGateway({
141
+ apiClient: props.apiClient,
142
+ projectId: props.projectId,
143
+ branchId,
144
+ gateway: {
145
+ baseUrl: gatewayBaseUrl,
146
+ token: gatewayToken
147
+ }
148
+ });
149
+ return {
150
+ status: "written",
151
+ written,
152
+ file: targetPath,
153
+ ...credential && credential.keys.length > 0 ? { credential } : {},
154
+ ...skipped && skipped.length > 0 ? { skipped } : {}
155
+ };
200
156
  };
201
157
  /**
202
- * The keys this pull is allowed to prune, i.e. the ones it is authoritative for.
203
- *
204
- * A `--service` selection narrows that to the services it named: `env pull -s ai-gateway`
205
- * says nothing about `DATABASE_URL`, so it must not read that variable's absence from this
206
- * pull as "the branch no longer has it". `unreached` is subtracted for the same reason — see
207
- * {@link unreachedButCurrent}.
208
- */
158
+ * The keys this pull is allowed to prune, i.e. the ones it is authoritative for.
159
+ *
160
+ * A `--service` selection narrows that to the services it named: `env pull -s ai-gateway`
161
+ * says nothing about `DATABASE_URL`, so it must not read that variable's absence from this
162
+ * pull as "the branch no longer has it". `unreached` is subtracted for the same reason — see
163
+ * {@link unreachedButCurrent}.
164
+ */
209
165
  const managedKeysFor = (services, unreached) => {
210
- const owned = services
211
- ? ownedEnvServiceKeys(services)
212
- : [...NEON_OWNED_ENV_KEYS];
213
- if (unreached.length === 0)
214
- return owned;
215
- const keep = new Set(ownedEnvServiceKeys(unreached));
216
- return owned.filter((key) => !keep.has(key));
166
+ const owned = services ? ownedEnvServiceKeys(services) : [...NEON_OWNED_ENV_KEYS];
167
+ if (unreached.length === 0) return owned;
168
+ const keep = new Set(ownedEnvServiceKeys(unreached));
169
+ return owned.filter((key) => !keep.has(key));
217
170
  };
218
171
  /**
219
- * Of the services this pull could not reach, the ones whose variables already on disk belong
220
- * to the branch being pulled — the only ones worth keeping.
221
- *
222
- * Failing to reach a service is not evidence that the branch stopped having it:
223
- * `PLATFORM_FEATURE_UNAVAILABLE` covers a transient incident as well as a project that
224
- * genuinely lacks the feature, and pruning would delete a token whose secret exists nowhere
225
- * else and strand the live credential behind it. But that only argues for keeping *this
226
- * branch's* values. Variables left over from another branch are stale by definition, and
227
- * keeping those would leave an app pointed at the wrong branch's gateway — a worse failure
228
- * than losing a token, because it is silent.
229
- *
230
- * The gateway is the only service that can be unreached (only it is implied rather than
231
- * observed), and its base URL is branch-scoped, so the persisted URL is what tells the two
232
- * cases apart. Anything that does not resolve to this branch's gateway host is pruned, which
233
- * is the safe direction: a stale entry costs a re-pull, a wrongly-kept one silently misroutes
234
- * traffic.
235
- */
172
+ * Of the services this pull could not reach, the ones whose variables already on disk belong
173
+ * to the branch being pulled — the only ones worth keeping.
174
+ *
175
+ * Failing to reach a service is not evidence that the branch stopped having it:
176
+ * `PLATFORM_FEATURE_UNAVAILABLE` covers a transient incident as well as a project that
177
+ * genuinely lacks the feature, and pruning would delete a token whose secret exists nowhere
178
+ * else and strand the live credential behind it. But that only argues for keeping *this
179
+ * branch's* values. Variables left over from another branch are stale by definition, and
180
+ * keeping those would leave an app pointed at the wrong branch's gateway — a worse failure
181
+ * than losing a token, because it is silent.
182
+ *
183
+ * The gateway is the only service that can be unreached (only it is implied rather than
184
+ * observed), and its base URL is branch-scoped, so the persisted URL is what tells the two
185
+ * cases apart. Anything that does not resolve to this branch's gateway host is pruned, which
186
+ * is the safe direction: a stale entry costs a re-pull, a wrongly-kept one silently misroutes
187
+ * traffic.
188
+ */
236
189
  const unreachedButCurrent = (skipped, existingEnv, branchId) => {
237
- if (!skipped?.includes("ai-gateway"))
238
- return [];
239
- const baseUrl = existingEnv[NEON_ENV_VAR_KEYS.aiGateway.baseUrl];
240
- return baseUrl !== undefined && isBranchGatewayUrl(baseUrl, branchId)
241
- ? ["ai-gateway"]
242
- : [];
190
+ if (!skipped?.includes("ai-gateway")) return [];
191
+ const baseUrl = existingEnv[NEON_ENV_VAR_KEYS.aiGateway.baseUrl];
192
+ return baseUrl !== void 0 && isBranchGatewayUrl(baseUrl, branchId) ? ["ai-gateway"] : [];
243
193
  };
244
194
  /**
245
- * Whether a persisted `NEON_AI_GATEWAY_BASE_URL` addresses `branchId`'s gateway.
246
- *
247
- * Checks the parsed **hostname** against the shape `@neon/env` builds
248
- * (`<branchId>-api.ai.<host suffix>`), not the raw string: a prefix comparison is satisfied
249
- * by a URL whose userinfo carries the branch id (`https://<branchId>-api.ai.@other-host/`)
250
- * while the request actually goes elsewhere. An unparseable value is not this branch's
251
- * gateway either, which is an answer rather than a swallowed failure.
252
- */
253
- const isBranchGatewayUrl = (baseUrl, branchId) => URL.canParse(baseUrl) &&
254
- new URL(baseUrl).hostname.startsWith(`${branchId}-api.ai.`);
195
+ * Whether a persisted `NEON_AI_GATEWAY_BASE_URL` addresses `branchId`'s gateway.
196
+ *
197
+ * Checks the parsed **hostname** against the shape `@neon/env` builds
198
+ * (`<branchId>-api.ai.<host suffix>`), not the raw string: a prefix comparison is satisfied
199
+ * by a URL whose userinfo carries the branch id (`https://<branchId>-api.ai.@other-host/`)
200
+ * while the request actually goes elsewhere. An unparseable value is not this branch's
201
+ * gateway either, which is an answer rather than a swallowed failure.
202
+ */
203
+ const isBranchGatewayUrl = (baseUrl, branchId) => URL.canParse(baseUrl) && new URL(baseUrl).hostname.startsWith(`${branchId}-api.ai.`);
255
204
  /**
256
- * Narrow the resolved vars to the selected services (plus `NEON_BRANCH`, which every pull
257
- * refreshes). Needed because the two `DATABASE_URL*` vars are always resolved — `fetchEnv`
258
- * reads both connection URIs regardless, since the AI Gateway host is derived from the direct
259
- * one — so `--service ai-gateway` has to drop them here rather than avoid fetching them.
260
- */
205
+ * Narrow the resolved vars to the selected services (plus `NEON_BRANCH`, which every pull
206
+ * refreshes). Needed because the two `DATABASE_URL*` vars are always resolved — `fetchEnv`
207
+ * reads both connection URIs regardless, since the AI Gateway host is derived from the direct
208
+ * one — so `--service ai-gateway` has to drop them here rather than avoid fetching them.
209
+ */
261
210
  const pickServiceVars = (vars, services) => {
262
- if (!services)
263
- return vars;
264
- const wanted = envServiceKeys(services);
265
- return Object.fromEntries(Object.entries(vars).filter(([key]) => wanted.has(key)));
211
+ if (!services) return vars;
212
+ const wanted = envServiceKeys(services);
213
+ return Object.fromEntries(Object.entries(vars).filter(([key]) => wanted.has(key)));
266
214
  };
267
215
  /**
268
- * Pull a freshly-pinned branch's Neon env vars into a local `.env`, bundled into `link` and
269
- * `checkout` so the branch-first loop is just *link + checkout* — `env pull` runs for you.
270
- *
271
- * On by default; `--no-env-pull` opts out (e.g. when env is injected at runtime via
272
- * `neon-env run` / `neon dev`, or to keep secrets out of the working tree). The pin is the
273
- * command's primary effect and has already succeeded by the time this runs, so a pull failure
274
- * degrades to a warning rather than failing the command. Returns what happened so
275
- * `link --agent` can fold an accurate note into its JSON message.
276
- */
277
- export const autoPullEnvAfterPin = async (props) => {
278
- if (!props.envPull) {
279
- log.info(chalk.dim(ENV_PULL_SKIPPED_HINT));
280
- return { status: "skipped" };
281
- }
282
- try {
283
- return await pull(props);
284
- }
285
- catch (err) {
286
- const message = err instanceof Error ? err.message : String(err);
287
- log.warning("Branch pinned, but pulling its Neon env vars failed: %s\n" +
288
- `Run \`${getCliName()} env pull\` once resolved (e.g. \`${getCliName()} deploy\` if a declared service ` +
289
- "is missing), or inject them at runtime with `neon-env run -- <your dev command>`.", message);
290
- return { status: "failed", message };
291
- }
216
+ * Pull a freshly-pinned branch's Neon env vars into a local `.env`, bundled into `link` and
217
+ * `checkout` so the branch-first loop is just *link + checkout* — `env pull` runs for you.
218
+ *
219
+ * On by default; `--no-env-pull` opts out (e.g. when env is injected at runtime via
220
+ * `neon-env run` / `neon dev`, or to keep secrets out of the working tree). The pin is the
221
+ * command's primary effect and has already succeeded by the time this runs, so a pull failure
222
+ * degrades to a warning rather than failing the command. Returns what happened so
223
+ * `link --agent` can fold an accurate note into its JSON message.
224
+ */
225
+ const autoPullEnvAfterPin = async (props) => {
226
+ if (!props.envPull) {
227
+ log.info(chalk.dim(ENV_PULL_SKIPPED_HINT));
228
+ return { status: "skipped" };
229
+ }
230
+ try {
231
+ return await pull(props);
232
+ } catch (err) {
233
+ const message = err instanceof Error ? err.message : String(err);
234
+ log.warning(`Branch pinned, but pulling its Neon env vars failed: %s
235
+ Run \`${getCliName()} env pull\` once resolved (e.g. \`${getCliName()} deploy\` if a declared service is missing), or inject them at runtime with \`neon-env run -- <your dev command>\`.`, message);
236
+ return {
237
+ status: "failed",
238
+ message
239
+ };
240
+ }
292
241
  };
293
242
  /**
294
- * Render the one-line env-pull note appended to `link --agent`'s JSON `message`, so an agent
295
- * reading the structured output knows whether its branch env is already on disk.
296
- */
297
- export const renderAgentPullNote = (result) => {
298
- switch (result.status) {
299
- case "written": {
300
- // Call out a re-issued credential: an agent that already wrote the old storage /
301
- // gateway values somewhere else has to update them.
302
- const credential = result.credential?.issued
303
- ? ` Issued a new branch credential, so ${result.credential.keys.join(", ")} changed.`
304
- : "";
305
- // No `skipped` note: only the implied AI Gateway can be skipped, and the auto-pull
306
- // this renders never implies it.
307
- return ` Pulled ${result.written.length} Neon env var${result.written.length === 1 ? "" : "s"} into ${result.file}.${credential}`;
308
- }
309
- case "empty":
310
- return " No Neon env vars to pull for this branch yet.";
311
- case "skipped":
312
- return (` Skipped env pull (--no-env-pull); run \`${getCliName()} env pull\` later, ` +
313
- "or inject env at runtime with `neon-env run -- <your dev command>`.");
314
- case "failed":
315
- return ` Could not pull env vars (${result.message}); run \`${getCliName()} env pull\` once resolved.`;
316
- }
243
+ * Render the one-line env-pull note appended to `link --agent`'s JSON `message`, so an agent
244
+ * reading the structured output knows whether its branch env is already on disk.
245
+ */
246
+ const renderAgentPullNote = (result) => {
247
+ switch (result.status) {
248
+ case "written": {
249
+ const credential = result.credential?.issued ? ` Issued a new branch credential, so ${result.credential.keys.join(", ")} changed.` : "";
250
+ return ` Pulled ${result.written.length} Neon env var${result.written.length === 1 ? "" : "s"} into ${result.file}.${credential}`;
251
+ }
252
+ case "empty": return " No Neon env vars to pull for this branch yet.";
253
+ case "skipped": return ` Skipped env pull (--no-env-pull); run \`${getCliName()} env pull\` later, or inject env at runtime with \`neon-env run -- <your dev command>\`.`;
254
+ case "failed": return ` Could not pull env vars (${result.message}); run \`${getCliName()} env pull\` once resolved.`;
255
+ }
317
256
  };
318
257
  /**
319
- * Keep only the recognized Neon variables from the resolved set, so a stray inherited
320
- * value never lands in the user's `.env` file. (Today `resolveNeonEnvVars` only emits Neon
321
- * vars, but filtering keeps the contract explicit and future-proof.)
322
- */
258
+ * Keep only the recognized Neon variables from the resolved set, so a stray inherited
259
+ * value never lands in the user's `.env` file. (Today `resolveNeonEnvVars` only emits Neon
260
+ * vars, but filtering keeps the contract explicit and future-proof.)
261
+ */
323
262
  const pickNeonVars = (vars) => {
324
- const out = {};
325
- for (const name of NEON_VAR_NAMES) {
326
- const value = vars[name];
327
- if (value !== undefined)
328
- out[name] = value;
329
- }
330
- return out;
263
+ const out = {};
264
+ for (const name of NEON_VAR_NAMES) {
265
+ const value = vars[name];
266
+ if (value !== void 0) out[name] = value;
267
+ }
268
+ return out;
331
269
  };
270
+ //#endregion
271
+ export { ENV_PULL_SKIPPED_HINT, autoPullEnvAfterPin, builder, command, describe, env_exports, handler, pull, renderAgentPullNote };