neonctl 2.37.0 → 2.38.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 (160) hide show
  1. package/{LICENSE.md → LICENSE} +27 -3
  2. package/README.md +15 -700
  3. package/bin/cli.js +3 -0
  4. package/package.json +8 -75
  5. package/dist/analytics.js +0 -168
  6. package/dist/api.js +0 -720
  7. package/dist/auth.js +0 -125
  8. package/dist/callback.html +0 -51
  9. package/dist/cli.js +0 -9
  10. package/dist/commands/api.js +0 -278
  11. package/dist/commands/auth.js +0 -214
  12. package/dist/commands/bootstrap.js +0 -481
  13. package/dist/commands/branches.js +0 -488
  14. package/dist/commands/bucket.js +0 -549
  15. package/dist/commands/checkout.js +0 -321
  16. package/dist/commands/config.js +0 -626
  17. package/dist/commands/connection_string.js +0 -172
  18. package/dist/commands/data_api.js +0 -284
  19. package/dist/commands/databases.js +0 -82
  20. package/dist/commands/deploy.js +0 -26
  21. package/dist/commands/dev.js +0 -698
  22. package/dist/commands/diff.js +0 -222
  23. package/dist/commands/env.js +0 -189
  24. package/dist/commands/functions.js +0 -373
  25. package/dist/commands/index.js +0 -62
  26. package/dist/commands/init.js +0 -73
  27. package/dist/commands/inspect.js +0 -65
  28. package/dist/commands/ip_allow.js +0 -137
  29. package/dist/commands/link.js +0 -1121
  30. package/dist/commands/neon_auth.js +0 -1028
  31. package/dist/commands/operations.js +0 -28
  32. package/dist/commands/orgs.js +0 -24
  33. package/dist/commands/projects.js +0 -413
  34. package/dist/commands/psql.js +0 -62
  35. package/dist/commands/roles.js +0 -65
  36. package/dist/commands/schema_diff.js +0 -151
  37. package/dist/commands/set_context.js +0 -29
  38. package/dist/commands/snapshots.js +0 -455
  39. package/dist/commands/status.js +0 -40
  40. package/dist/commands/user.js +0 -15
  41. package/dist/commands/vpc_endpoints.js +0 -134
  42. package/dist/config.js +0 -11
  43. package/dist/config_format.js +0 -72
  44. package/dist/context.js +0 -231
  45. package/dist/current_branch_fast_path.js +0 -55
  46. package/dist/dev/env.js +0 -244
  47. package/dist/dev/functions.js +0 -70
  48. package/dist/dev/inputs.js +0 -63
  49. package/dist/dev/runtime.js +0 -146
  50. package/dist/env.js +0 -36
  51. package/dist/env_file.js +0 -159
  52. package/dist/errors.js +0 -80
  53. package/dist/functions_api.js +0 -48
  54. package/dist/help.js +0 -146
  55. package/dist/index.js +0 -240
  56. package/dist/log.js +0 -18
  57. package/dist/parameters.gen.js +0 -480
  58. package/dist/pkg.js +0 -25
  59. package/dist/psql/cli.js +0 -53
  60. package/dist/psql/command/cmd_cond.js +0 -437
  61. package/dist/psql/command/cmd_connect.js +0 -820
  62. package/dist/psql/command/cmd_copy.js +0 -1035
  63. package/dist/psql/command/cmd_describe.js +0 -1815
  64. package/dist/psql/command/cmd_format.js +0 -948
  65. package/dist/psql/command/cmd_io.js +0 -2193
  66. package/dist/psql/command/cmd_lo.js +0 -393
  67. package/dist/psql/command/cmd_meta.js +0 -969
  68. package/dist/psql/command/cmd_misc.js +0 -187
  69. package/dist/psql/command/cmd_pipeline.js +0 -1148
  70. package/dist/psql/command/cmd_restrict.js +0 -171
  71. package/dist/psql/command/cmd_show.js +0 -766
  72. package/dist/psql/command/dispatch.js +0 -343
  73. package/dist/psql/command/inputQueue.js +0 -42
  74. package/dist/psql/command/shared.js +0 -71
  75. package/dist/psql/complete/filenames.js +0 -139
  76. package/dist/psql/complete/index.js +0 -104
  77. package/dist/psql/complete/matcher.js +0 -315
  78. package/dist/psql/complete/psqlVars.js +0 -249
  79. package/dist/psql/complete/queries.js +0 -493
  80. package/dist/psql/complete/rules.js +0 -2424
  81. package/dist/psql/core/common.js +0 -1253
  82. package/dist/psql/core/help.js +0 -576
  83. package/dist/psql/core/mainloop.js +0 -1360
  84. package/dist/psql/core/prompt.js +0 -439
  85. package/dist/psql/core/settings.js +0 -686
  86. package/dist/psql/core/sqlHelp.js +0 -1066
  87. package/dist/psql/core/startup.js +0 -846
  88. package/dist/psql/core/syncVars.js +0 -116
  89. package/dist/psql/core/variables.js +0 -287
  90. package/dist/psql/describe/formatters.js +0 -1330
  91. package/dist/psql/describe/processNamePattern.js +0 -270
  92. package/dist/psql/describe/queries.js +0 -2452
  93. package/dist/psql/describe/versionGate.js +0 -44
  94. package/dist/psql/index.js +0 -2030
  95. package/dist/psql/io/history.js +0 -299
  96. package/dist/psql/io/input.js +0 -120
  97. package/dist/psql/io/lineEditor/buffer.js +0 -325
  98. package/dist/psql/io/lineEditor/complete.js +0 -227
  99. package/dist/psql/io/lineEditor/filename.js +0 -159
  100. package/dist/psql/io/lineEditor/index.js +0 -893
  101. package/dist/psql/io/lineEditor/keymap.js +0 -745
  102. package/dist/psql/io/lineEditor/vt100.js +0 -363
  103. package/dist/psql/io/pgpass.js +0 -202
  104. package/dist/psql/io/pgservice.js +0 -194
  105. package/dist/psql/io/psqlrc.js +0 -422
  106. package/dist/psql/print/aligned.js +0 -1748
  107. package/dist/psql/print/asciidoc.js +0 -230
  108. package/dist/psql/print/crosstab.js +0 -463
  109. package/dist/psql/print/csv.js +0 -76
  110. package/dist/psql/print/html.js +0 -240
  111. package/dist/psql/print/json.js +0 -96
  112. package/dist/psql/print/latex.js +0 -379
  113. package/dist/psql/print/pager.js +0 -267
  114. package/dist/psql/print/troff.js +0 -240
  115. package/dist/psql/print/unaligned.js +0 -99
  116. package/dist/psql/print/units.js +0 -188
  117. package/dist/psql/scanner/slash.js +0 -515
  118. package/dist/psql/scanner/sql.js +0 -914
  119. package/dist/psql/scanner/stringutils.js +0 -394
  120. package/dist/psql/types/backslash.js +0 -1
  121. package/dist/psql/types/connection.js +0 -1
  122. package/dist/psql/types/index.js +0 -7
  123. package/dist/psql/types/printer.js +0 -1
  124. package/dist/psql/types/repl.js +0 -1
  125. package/dist/psql/types/scanner.js +0 -24
  126. package/dist/psql/types/settings.js +0 -1
  127. package/dist/psql/types/variables.js +0 -1
  128. package/dist/psql/wire/connection.js +0 -2857
  129. package/dist/psql/wire/copy.js +0 -108
  130. package/dist/psql/wire/notify.js +0 -59
  131. package/dist/psql/wire/pipeline.js +0 -521
  132. package/dist/psql/wire/protocol.js +0 -466
  133. package/dist/psql/wire/sasl.js +0 -294
  134. package/dist/psql/wire/tls.js +0 -602
  135. package/dist/storage_api.js +0 -147
  136. package/dist/test_utils/fixtures.js +0 -121
  137. package/dist/test_utils/oauth_server.js +0 -9
  138. package/dist/types.js +0 -1
  139. package/dist/utils/ai_gateway_notice.js +0 -180
  140. package/dist/utils/api_enums.js +0 -33
  141. package/dist/utils/auth.js +0 -5
  142. package/dist/utils/branch_notice.js +0 -22
  143. package/dist/utils/branch_picker.js +0 -103
  144. package/dist/utils/compute_units.js +0 -28
  145. package/dist/utils/config_diff.js +0 -185
  146. package/dist/utils/enrichers.js +0 -161
  147. package/dist/utils/esbuild.js +0 -158
  148. package/dist/utils/formats.js +0 -18
  149. package/dist/utils/git_diff.js +0 -90
  150. package/dist/utils/inspect_db.js +0 -153
  151. package/dist/utils/inspect_queries.js +0 -372
  152. package/dist/utils/middlewares.js +0 -20
  153. package/dist/utils/openapi.js +0 -114
  154. package/dist/utils/package_manager.js +0 -68
  155. package/dist/utils/point_in_time.js +0 -56
  156. package/dist/utils/psql.js +0 -120
  157. package/dist/utils/string.js +0 -5
  158. package/dist/utils/ui.js +0 -59
  159. package/dist/utils/zip.js +0 -4
  160. package/dist/writer.js +0 -97
@@ -1,626 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { resolveConfig } from "@neon/config";
4
- import { apply, createBranch as createBranchFromPolicy, inspect, isPartialBranchCreateError, loadConfigFromFile, PushConflictError, plan, } from "@neon/config-runtime";
5
- import chalk from "chalk";
6
- import { getApiClient } from "../api.js";
7
- import { toNeonConfigView } from "../config_format.js";
8
- import { contextBranch, readContextFile } from "../context.js";
9
- import { isCi } from "../env.js";
10
- import { loadEnvFileIntoProcess } from "../env_file.js";
11
- import { log } from "../log.js";
12
- import { assertAiGatewayProvisionable, warnAiGateway, } from "../utils/ai_gateway_notice.js";
13
- import { announceTargetBranch } from "../utils/branch_notice.js";
14
- import { renderAppliedChanges, renderBranchSettingConflicts, } from "../utils/config_diff.js";
15
- import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
16
- import { bundleEntry } from "../utils/esbuild.js";
17
- import { addDependenciesArgs, resolvePackageManager, runCommand, } from "../utils/package_manager.js";
18
- import { zipBundle } from "../utils/zip.js";
19
- import { writer } from "../writer.js";
20
- import { autoPullEnvAfterPin } from "./env.js";
21
- /**
22
- * Bundle a function with neonctl's OWN bundler (the shared esbuild helper) so the
23
- * config-runtime never has to import esbuild itself. Injecting this keeps esbuild
24
- * out of config-runtime's static module graph — and therefore out of the packaged
25
- * neonctl snapshot, which resolves esbuild dynamically at deploy time.
26
- */
27
- const neonctlBundler = async (fn) => zipBundle(await bundleEntry(fn.source));
28
- const INSPECT_FIELDS = ["project", "branch", "config"];
29
- /**
30
- * Shared `--env` flag for `config plan|apply` and `deploy`. Loads a `.env` into
31
- * `process.env` before the policy is evaluated.
32
- */
33
- export const envFlag = {
34
- env: {
35
- describe: "Path to a .env file to load into the environment before evaluating neon.ts " +
36
- "(so function env values resolve from it). Existing env vars are not overridden.",
37
- type: "string",
38
- },
39
- };
40
- /** Apply-only flags, exported so `deploy` can reuse the exact same surface. */
41
- export const applyFlags = {
42
- "update-existing": {
43
- describe: "Auto-confirm overriding existing remote settings on the branch",
44
- type: "boolean",
45
- default: false,
46
- },
47
- "allow-protected": {
48
- describe: "Auto-confirm applying to a branch marked protected on Neon",
49
- type: "boolean",
50
- default: false,
51
- },
52
- };
53
- /**
54
- * `--env-pull` for `config apply` / `deploy` (shared so both expose the identical surface).
55
- * After a successful apply, the branch's Neon env vars are written to a local `.env` — the
56
- * same bundled convenience as `link` / `checkout`. On by default; `--no-env-pull` opts out.
57
- */
58
- export const envPullFlag = {
59
- "env-pull": {
60
- describe: "Pull the branch's Neon env vars (DATABASE_URL, …) into a local .env after a " +
61
- "successful apply. On by default; use --no-env-pull to skip (e.g. when injecting " +
62
- "env at runtime with `neon-env run` / `neon dev`).",
63
- type: "boolean",
64
- default: true,
65
- },
66
- };
67
- // ── `config init` ─────────────────────────────────────────────────────────────
68
- /**
69
- * The published npm packages a `neon.ts` project needs — the `@neon/*` org names.
70
- *
71
- * ⚠️ These ship to users the next time `neonctl` is released, so do NOT release
72
- * neonctl until `@neon/config` and `@neon/env` are published to npm — otherwise
73
- * `config init` would install packages that don't exist yet. (The libraries are
74
- * mid-migration from `@neondatabase/*`; track their publish before cutting a CLI
75
- * release.)
76
- */
77
- const CONFIG_PACKAGE = "@neon/config";
78
- const ENV_PACKAGE = "@neon/env";
79
- const REQUIRED_PACKAGES = [CONFIG_PACKAGE, ENV_PACKAGE];
80
- /** package.json fields a dependency can be declared in. */
81
- const DEPENDENCY_FIELDS = [
82
- "dependencies",
83
- "devDependencies",
84
- "peerDependencies",
85
- "optionalDependencies",
86
- ];
87
- /** Config filenames the runtime loads (mirrors @neon/config's loader). */
88
- const NEON_CONFIG_FILENAMES = ["neon.ts", "neon.mts", "neon.js", "neon.mjs"];
89
- /** Whether `dir` already has a Neon config file the runtime would load. */
90
- export const hasNeonConfigFile = (dir) => NEON_CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
91
- /** Starter `neon.ts` written by `config init` when a project has none. */
92
- const NEON_CONFIG_TEMPLATE = `import { defineConfig } from "${CONFIG_PACKAGE}/v1";
93
-
94
- export default defineConfig({
95
- // Declare your Neon services here
96
- auth: false,
97
- // Branch policy: per-branch tuning
98
- branch: (branch) => {
99
- if (branch.isDefault) {
100
- // Default branch: no overrides, uses project defaults
101
- return {};
102
- }
103
- if (!branch.exists) {
104
- // New non-default branches: auto-expire
105
- // Run \`neon checkout <name>\` to create a new branch with these settings
106
- return { ttl: "7d" };
107
- }
108
- // Existing branch: no changes
109
- return {};
110
- },
111
- });
112
- `;
113
- const isRecord = (value) => typeof value === "object" && value !== null;
114
- /**
115
- * The {@link REQUIRED_PACKAGES} not already declared in the project's package.json
116
- * (any dependency field). A missing or malformed package.json means none are
117
- * declared, so all are reported missing.
118
- */
119
- const missingDependencies = (cwd) => {
120
- const declared = new Set();
121
- const pkgPath = join(cwd, "package.json");
122
- if (existsSync(pkgPath)) {
123
- let parsed;
124
- try {
125
- parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
126
- }
127
- catch {
128
- parsed = undefined;
129
- }
130
- if (isRecord(parsed)) {
131
- for (const field of DEPENDENCY_FIELDS) {
132
- const deps = parsed[field];
133
- if (isRecord(deps)) {
134
- for (const name of Object.keys(deps))
135
- declared.add(name);
136
- }
137
- }
138
- }
139
- }
140
- return REQUIRED_PACKAGES.filter((pkg) => !declared.has(pkg));
141
- };
142
- /**
143
- * Scaffold a `neon.ts` policy and make sure the Neon config packages are
144
- * installed, so a project can go straight to `neon config plan` / `apply`.
145
- * Purely local — it never touches the Neon API (see {@link isConfigInit}).
146
- */
147
- export const initCmd = async (props) => {
148
- const cwd = props.cwd ?? process.cwd();
149
- const run = props.run ?? runCommand;
150
- // 1. Scaffold neon.ts unless the project already has a Neon config file.
151
- const existing = NEON_CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
152
- if (existing) {
153
- log.info("Found an existing %s — leaving it untouched.", existing);
154
- }
155
- else {
156
- writeFileSync(join(cwd, "neon.ts"), NEON_CONFIG_TEMPLATE);
157
- log.info("Created neon.ts with a starter policy.");
158
- }
159
- // 2. Make sure the config packages are installed.
160
- const missing = missingDependencies(cwd);
161
- if (missing.length === 0) {
162
- log.info("%s are already installed.", REQUIRED_PACKAGES.join(" and "));
163
- }
164
- else {
165
- const pm = resolvePackageManager();
166
- const args = addDependenciesArgs(pm, missing);
167
- if (props.install === false) {
168
- log.info("Install the Neon config packages to use neon.ts: %s %s", pm, args.join(" "));
169
- }
170
- else {
171
- log.info("Installing %s with %s…", missing.join(", "), pm);
172
- const ok = await run(pm, args, cwd);
173
- if (!ok) {
174
- log.warning("Could not install the config packages automatically. Run by hand: %s %s", pm, args.join(" "));
175
- }
176
- }
177
- }
178
- log.info("Next: edit neon.ts, then run `neon config plan` to preview and `neon config apply`.");
179
- };
180
- export const command = "config";
181
- export const describe = "Manage a branch with a neon.ts policy";
182
- export const builder = (argv) => argv
183
- .usage("$0 config <sub-command> [options]")
184
- .options({
185
- "project-id": {
186
- describe: "Project ID",
187
- type: "string",
188
- },
189
- branch: {
190
- describe: "Branch ID or name",
191
- type: "string",
192
- },
193
- })
194
- .middleware(fillSingleProject)
195
- .command("status", "Show the branch's live Neon state", (yargs) => yargs.options({
196
- "config-json": {
197
- describe: "Print only the branch's live config as neon.ts-shaped JSON " +
198
- "(services + branch tuning + preview), to stdout. Useful for " +
199
- "scripting or copying into a neon.ts.",
200
- type: "boolean",
201
- default: false,
202
- },
203
- "current-branch": {
204
- describe: "Print only the linked branch name from the local .neon file " +
205
- "(no network). Exits non-zero when no branch is pinned.",
206
- type: "boolean",
207
- default: false,
208
- },
209
- }), (args) => status(args))
210
- .command("plan", "Show what `config apply` would change (dry run)", (yargs) => yargs.options({
211
- config: {
212
- describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
213
- type: "string",
214
- },
215
- ...envFlag,
216
- }), (args) => planCmd(args))
217
- .command("apply", "Apply a neon.ts policy to the branch", (yargs) => yargs.options({
218
- config: {
219
- describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
220
- type: "string",
221
- },
222
- ...envFlag,
223
- ...applyFlags,
224
- ...envPullFlag,
225
- }), (args) => applyCmd(args))
226
- .command("init", "Scaffold a neon.ts policy and install the Neon config packages", (yargs) => yargs.options({
227
- install: {
228
- describe: "Install @neon/config and @neon/env if they're missing. " +
229
- "On by default; use --no-install to just print the command.",
230
- type: "boolean",
231
- default: true,
232
- },
233
- }), (args) => initCmd(args));
234
- export const handler = (args) => {
235
- return args;
236
- };
237
- const loadConfig = async (props) => {
238
- // Load the optional --env file FIRST so a `neon.ts` whose function `env` values read
239
- // `process.env.X` sees them. Must happen before the policy module is imported/evaluated.
240
- if (props.env) {
241
- const applied = loadEnvFileIntoProcess(props.env);
242
- log.debug("Loaded %d var(s) from %s into the environment: %s", applied.length, props.env, applied.join(", "));
243
- }
244
- const { config } = await loadConfigFromFile({
245
- ...(props.config ? { path: props.config } : {}),
246
- });
247
- return config;
248
- };
249
- export const status = async (props) => {
250
- // `--current-branch` short-circuits here (before resolveBranchRef), so it wins
251
- // over --config-json and ignores --output. See ConfigProps.currentBranch / isCurrentBranchProbe.
252
- if (props.currentBranch) {
253
- const branch = contextBranch(readContextFile(props.contextFile));
254
- if (branch) {
255
- process.stdout.write(`${branch}\n`);
256
- }
257
- else {
258
- // No branch pinned: hint on stderr and exit non-zero (grep-style) so a prompt's
259
- // `when` hides the segment cleanly instead of rendering a bare icon.
260
- log.info("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
261
- process.exitCode = 1;
262
- }
263
- return;
264
- }
265
- const branch = await resolveBranchRef(props);
266
- // `--config-json` is a script-friendly mode that emits only JSON to stdout, so keep it
267
- // pristine; the regular human view gets the "which branch am I inspecting" guardrail.
268
- if (!props.configJson) {
269
- announceTargetBranch(props, branch, "Inspecting branch");
270
- }
271
- const branchId = branch.branchId;
272
- const live = await inspect({
273
- projectId: props.projectId,
274
- branchId,
275
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
276
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
277
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
278
- });
279
- // The pulled `config` carries the branch's tuning inside a closure that JSON can't
280
- // render. Resolve it against the live branch target to get the concrete settings, then
281
- // project both that and the separately-pulled preview state into a neon.ts-shaped view.
282
- const resolved = resolveConfig(live.config, {
283
- name: live.branch.name,
284
- id: live.branch.id,
285
- exists: true,
286
- isDefault: live.branch.isDefault,
287
- isProtected: live.branch.protected,
288
- ...(live.branch.parent ? { parentId: live.branch.parent } : {}),
289
- ...(live.branch.expiresAt ? { expiresAt: live.branch.expiresAt } : {}),
290
- });
291
- const configView = toNeonConfigView(resolved, live.preview);
292
- // `--config-json`: emit just the neon.ts-shaped config to stdout (script-friendly,
293
- // copy-paste-able), regardless of the global --output.
294
- if (props.configJson) {
295
- process.stdout.write(`${JSON.stringify(configView, null, 2)}\n`);
296
- return;
297
- }
298
- // Default: the live project/branch tables, but with the unhelpful raw `config` replaced
299
- // by the resolved neon.ts-shaped view so the user sees enabled infra + branch tuning.
300
- writer(props).end({ project: live.project, branch: live.branch, config: configView }, { fields: INSPECT_FIELDS });
301
- };
302
- export const planCmd = async (props) => {
303
- const config = await loadConfig(props);
304
- const branch = await resolveBranchRef(props);
305
- announceTargetBranch(props, branch, "Planning against branch");
306
- const branchId = branch.branchId;
307
- // `plan` is a dry run that never bundles, so its options don't accept (or need)
308
- // an injected bundler — only `apply` does (it uses neonctlBundler).
309
- const result = await plan(config, {
310
- projectId: props.projectId,
311
- branchId,
312
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
313
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
314
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
315
- });
316
- const services = utilizedServices(config);
317
- reportPushResult(props, result, "plan", services);
318
- // `plan` is a dry run and never pulls credentials, so it can only offer the plan-based
319
- // (Free) AI Gateway notice — the reduced-model-set check needs a live gateway token,
320
- // which `apply`/`checkout`/`env pull` get via the bundled env pull. Best-effort.
321
- if (services.includes("AI Gateway")) {
322
- await warnAiGateway({
323
- apiClient: props.apiClient,
324
- projectId: props.projectId,
325
- branchId,
326
- });
327
- }
328
- };
329
- export const applyCmd = async (props) => {
330
- const config = await loadConfig(props);
331
- const branch = await resolveBranchRef(props);
332
- announceTargetBranch(props, branch, "Applying to branch");
333
- const branchId = branch.branchId;
334
- // The AI Gateway can't serve on the Free plan, so refuse to provision it up front rather
335
- // than write a credential that won't work. Only when the policy actually enables the
336
- // gateway; best-effort on the plan lookup (a transient failure never blocks a paid user).
337
- if (utilizedServices(config).includes("AI Gateway")) {
338
- await assertAiGatewayProvisionable({
339
- apiClient: props.apiClient,
340
- projectId: props.projectId,
341
- });
342
- }
343
- let result;
344
- try {
345
- result = await apply(config, {
346
- projectId: props.projectId,
347
- branchId,
348
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
349
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
350
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
351
- ...(props.updateExisting ? { updateExisting: true } : {}),
352
- ...(props.allowProtected ? { allowProtectedBranch: true } : {}),
353
- bundleFunction: neonctlBundler,
354
- });
355
- }
356
- catch (err) {
357
- // Drift without `--update-existing` throws with the conflicting fields attached.
358
- // Render them as the same git-style before→after diff, then fail with a concise
359
- // message (the detailed diff above replaces the library's long multi-line text).
360
- if (err instanceof PushConflictError) {
361
- reportConflicts(props, err.conflicts);
362
- throw new Error("Branch settings conflict with the policy. Re-run with --update-existing to apply the changes shown above.");
363
- }
364
- throw err;
365
- }
366
- reportPushResult(props, result, "apply", utilizedServices(config));
367
- // After a successful apply/deploy, write the branch's Neon env vars to a local .env —
368
- // the same bundled convenience as `link` / `checkout`, so the branch is immediately
369
- // usable for local dev. `--no-env-pull` opts out; a pull failure degrades to a warning
370
- // (the apply already succeeded). See autoPullEnvAfterPin.
371
- await autoPullEnvAfterPin({ ...props, envPull: props.envPull !== false });
372
- };
373
- /**
374
- * A static service toggle (`auth` / `dataApi` / `preview.aiGateway`) is "on" unless
375
- * explicitly disabled: `true` / `{}` / `{ enabled: true }` enable it; `false` /
376
- * `{ enabled: false }` / absent leave it off. Mirrors the runtime's `isServiceEnabled`
377
- * (which isn't exported), kept tiny and pure so it can be read straight off the policy.
378
- */
379
- const isToggleEnabled = (toggle) => {
380
- if (toggle === undefined)
381
- return false;
382
- if (typeof toggle === "boolean")
383
- return toggle;
384
- return toggle.enabled !== false;
385
- };
386
- /**
387
- * Human-readable list of the services a `neon.ts` policy utilizes on the branch, shown under
388
- * the plan/apply table. Postgres is always present (every branch has it); the rest are listed
389
- * only when the policy declares them. This deliberately surfaces services that produce **no**
390
- * plan step — notably the AI Gateway, which is always available and only needs a scoped branch
391
- * credential (not a provisioning step) — so adding `preview.aiGateway` to a neon.ts isn't
392
- * mistaken for being silently dropped. Service enablement is static top-level config (it never
393
- * lives in the per-branch closure), so reading it straight off `config` is accurate.
394
- */
395
- const utilizedServices = (config) => {
396
- const services = ["Postgres"];
397
- if (isToggleEnabled(config.auth))
398
- services.push("Neon Auth");
399
- if (isToggleEnabled(config.dataApi))
400
- services.push("Data API");
401
- if (Object.keys(config.preview?.buckets ?? {}).length > 0) {
402
- services.push("Object Storage");
403
- }
404
- if (Object.keys(config.preview?.functions ?? {}).length > 0) {
405
- services.push("Functions");
406
- }
407
- if (isToggleEnabled(config.preview?.aiGateway))
408
- services.push("AI Gateway");
409
- return services;
410
- };
411
- /**
412
- * Render a {@link PushResult}. JSON/YAML output emits the raw result (plus a `services`
413
- * summary) verbatim so it can be piped; the human-readable path renders the actual changes
414
- * (dropping noops) and any blocking conflicts as a `git diff`-style report, or a "nothing to
415
- * do" line when both are empty — and always closes with the list of services the policy
416
- * utilizes so a service that produces no plan step (Postgres, or the credential-gated AI
417
- * Gateway) isn't mistaken for being missing from the plan above.
418
- *
419
- * The diff is asymmetric on purpose (see the CLI's `neon diff`): **service** changes are
420
- * additions with no "before", so they list as `+`/`~` lines; **branch setting** changes have
421
- * a natural before→after, so conflicts render as a sorted `current → desired` diff. Planned
422
- * branch updates (under `--update-existing`) carry only the new value, so they render
423
- * desired-only for now (the previous value isn't threaded through the runtime yet).
424
- */
425
- const reportPushResult = (props, result, mode, services) => {
426
- if (props.output === "json" || props.output === "yaml") {
427
- writer(props).end({ ...result, services }, { fields: [] });
428
- return;
429
- }
430
- const appliedChanges = result.applied.filter((change) => change.action !== "noop");
431
- // Deployed functions carry their invocation URL in the change details — collect them so
432
- // we can list where to call each function without digging through the raw details blob.
433
- // Keyed by slug so a function never shows twice.
434
- const functionUrlBySlug = new Map();
435
- for (const change of appliedChanges) {
436
- const slug = change.details?.slug;
437
- const invocationUrl = change.details?.invocationUrl;
438
- if (typeof slug === "string" && typeof invocationUrl === "string") {
439
- functionUrlBySlug.set(slug, invocationUrl);
440
- }
441
- }
442
- // chalk self-detects TTY/NO_COLOR; `--no-color` (props.color === false) forces plain.
443
- const color = props.color !== false;
444
- const out = writer(props);
445
- // Conflicts never reach here in the CLI: `plan` runs with updateExisting on, and a bare
446
- // `apply` throws PushConflictError (rendered by reportConflicts). So an empty applied set
447
- // is the whole story here.
448
- const noChanges = appliedChanges.length === 0;
449
- const appliedText = renderAppliedChanges(appliedChanges, mode === "plan" ? "Planned changes" : "Applied changes", { color });
450
- if (appliedText)
451
- out.text(`${appliedText}\n`);
452
- // Function URLs are a plain list rather than a table: an invocation URL can be 70+ chars,
453
- // which makes any bordered table overflow and wrap awkwardly in a normal terminal. A list
454
- // lets each URL reflow on its own line, and stays copy-pasteable.
455
- if (functionUrlBySlug.size > 0) {
456
- const heading = mode === "plan" ? "Function URLs (after apply)" : "Function URLs";
457
- out.text(`\n${isCi() ? heading : chalk.bold(heading)}\n`);
458
- for (const [slug, invocationUrl] of functionUrlBySlug) {
459
- out.text(` • ${slug}: ${invocationUrl}\n`);
460
- }
461
- }
462
- if (noChanges) {
463
- log.info(`No changes — branch ${result.branchName} already matches the policy.`);
464
- }
465
- out.text(`\nUtilized services: ${services.join(", ")}\n`);
466
- };
467
- /**
468
- * Render the branch-setting {@link ConflictReport}s a bare `apply` refused to override (drift
469
- * without `--update-existing`) as the git-style before→after diff. JSON/YAML output emits the
470
- * structured conflicts so it can be piped; the human path prints the sorted diff followed by
471
- * any conflict whose fix is *not* `--update-existing` (e.g. an immutable "no endpoint" case),
472
- * so nothing the library's error message carried is lost.
473
- */
474
- const reportConflicts = (props, conflicts) => {
475
- if (props.output === "json" || props.output === "yaml") {
476
- writer(props).end({ conflicts }, { fields: [] });
477
- return;
478
- }
479
- const out = writer(props);
480
- const text = renderBranchSettingConflicts([...conflicts], {
481
- color: props.color !== false,
482
- });
483
- if (text)
484
- out.text(`${text}\n`);
485
- for (const conflict of conflicts) {
486
- if (!/updateExisting/i.test(conflict.reason)) {
487
- out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
488
- }
489
- }
490
- };
491
- /**
492
- * Block provisioning the AI Gateway on a Free plan from the `checkout` policy paths, which
493
- * carry raw credentials (`apiKey`/`apiHost`) rather than the CLI's api client. Builds a client
494
- * from them and defers to {@link assertAiGatewayProvisionable}. Skipped when a `runtimeApi` is
495
- * injected (tests) or no `apiKey` is available; the interactive commands always have a key.
496
- */
497
- const assertAiGatewayProvisionableFromCreds = async (props) => {
498
- if (props.runtimeApi || !props.apiKey)
499
- return;
500
- if (!utilizedServices(props.config).includes("AI Gateway"))
501
- return;
502
- const apiClient = getApiClient({
503
- apiKey: props.apiKey,
504
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
505
- });
506
- await assertAiGatewayProvisionable({
507
- apiClient,
508
- projectId: props.projectId,
509
- });
510
- };
511
- /**
512
- * Apply a `neon.ts` policy to a **freshly created** branch (used by `neonctl checkout`
513
- * when it creates a branch). No-op when there is no `neon.ts` on the path from cwd up to
514
- * the repo root — checkout still succeeds, it just has no policy to apply.
515
- *
516
- * The branch was just created by us, so we apply non-interactively (`updateExisting` /
517
- * `allowProtectedBranch`) — there is no pre-existing state a user would be surprised to
518
- * see overridden. Functions are bundled with neonctl's own esbuild helper.
519
- */
520
- export const applyPolicyOnCreate = async (props) => {
521
- let config;
522
- try {
523
- ({ config } = await loadConfigFromFile({
524
- ...(props.cwd ? { cwd: props.cwd } : {}),
525
- }));
526
- }
527
- catch (err) {
528
- const message = err instanceof Error ? err.message : String(err);
529
- if (/Could not find a Neon config file/i.test(message))
530
- return;
531
- throw err;
532
- }
533
- await assertAiGatewayProvisionableFromCreds({
534
- projectId: props.projectId,
535
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
536
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
537
- ...(props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}),
538
- config,
539
- });
540
- log.info("Applying neon.ts policy to the new branch…");
541
- const result = await apply(config, {
542
- projectId: props.projectId,
543
- branchId: props.branchId,
544
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
545
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
546
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
547
- updateExisting: true,
548
- allowProtectedBranch: true,
549
- bundleFunction: neonctlBundler,
550
- });
551
- logPolicyResult(result, { color: props.color !== false });
552
- };
553
- /**
554
- * Report what applying a `neon.ts` policy changed, using the same `field → value` diff
555
- * `deploy` prints (see {@link renderAppliedChanges}) rather than a bare list of change
556
- * identifiers — the identifier alone repeats the branch name once per change and never says
557
- * *what* was applied, which is the only interesting part on a freshly created branch.
558
- */
559
- const logPolicyResult = (result, opts) => {
560
- const changes = result.applied.filter((c) => c.action !== "noop");
561
- if (changes.length === 0) {
562
- log.info("neon.ts applied — no changes were needed.");
563
- return;
564
- }
565
- log.info("%s", renderAppliedChanges(changes, `neon.ts applied — ${changes.length} change${changes.length === 1 ? "" : "s"}:`, opts));
566
- };
567
- /**
568
- * Create a branch **from** the local `neon.ts` policy. Returns `null` when there is no
569
- * `neon.ts` on the path from cwd up to the repo root, so `neonctl checkout` can fall back to a
570
- * bare branch create.
571
- *
572
- * Unlike a bare create followed by {@link applyPolicyOnCreate}, this evaluates the policy for
573
- * the **new** branch (`exists: false`): the runtime branches from the policy's `parent` and
574
- * brings the branch up with its declared TTL / compute settings / services. That's what makes
575
- * a policy keyed on `!branch.exists` (the common "only configure new branches" shape) take
576
- * effect on the very first `checkout` — a bare create + `apply` always saw `exists: true` and
577
- * skipped that block.
578
- *
579
- * A branch that was created but whose policy failed to apply is reported through
580
- * `policyFailure` rather than thrown: the branch is real, so `checkout` still needs to pin it
581
- * (see the handler) instead of leaving it stranded behind an unchanged `.neon`.
582
- */
583
- export const createBranchFromPolicyOnCheckout = async (props) => {
584
- let config;
585
- try {
586
- ({ config } = await loadConfigFromFile({
587
- ...(props.cwd ? { cwd: props.cwd } : {}),
588
- }));
589
- }
590
- catch (err) {
591
- const message = err instanceof Error ? err.message : String(err);
592
- if (/Could not find a Neon config file/i.test(message))
593
- return null;
594
- throw err;
595
- }
596
- await assertAiGatewayProvisionableFromCreds({
597
- projectId: props.projectId,
598
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
599
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
600
- ...(props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}),
601
- config,
602
- });
603
- try {
604
- const { branchId, branchName, result } = await createBranchFromPolicy(config, {
605
- projectId: props.projectId,
606
- branchName: props.branchName,
607
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
608
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
609
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
610
- bundleFunction: neonctlBundler,
611
- });
612
- log.info("Created branch %s (%s) from neon.ts policy.", branchName, branchId);
613
- logPolicyResult(result, { color: props.color !== false });
614
- return { branchId };
615
- }
616
- catch (err) {
617
- // The branch exists but its policy didn't fully apply. Hand the id back so checkout
618
- // pins it and reports the failure with the remediation, rather than aborting with an
619
- // unpinned context and a branch the next `checkout` would silently accept as-is.
620
- if (isPartialBranchCreateError(err)) {
621
- log.info("Created branch %s (%s) from neon.ts policy.", err.branchName, err.branchId);
622
- return { branchId: err.branchId, policyFailure: err.reason };
623
- }
624
- throw err;
625
- }
626
- };