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,724 +1,641 @@
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 { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE, FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView, } from "../config_template.js";
9
- import { contextBranch, readContextFile } from "../context.js";
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
10
2
  import { isCi } from "../env.js";
11
- import { loadEnvFileIntoProcess } from "../env_file.js";
12
3
  import { log } from "../log.js";
13
- import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption, } from "../neon_services.js";
14
- import { assertAiGatewayProvisionable, warnAiGateway, } from "../utils/ai_gateway_notice.js";
15
- import { announceTargetBranch } from "../utils/branch_notice.js";
4
+ import { getApiClient } from "../api.js";
5
+ import { contextBranch, readContextFile } from "../context.js";
16
6
  import { getCliName } from "../utils/cli_name.js";
17
- import { renderAppliedChanges, renderBranchSettingConflicts, } from "../utils/config_diff.js";
7
+ import { toNeonConfigView } from "../config_format.js";
8
+ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
9
+ import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE, FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView } from "../config_template.js";
10
+ import { loadEnvFileIntoProcess } from "../env_file.js";
11
+ import { writer } from "../writer.js";
12
+ import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
18
13
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
14
+ import { assertAiGatewayProvisionable, warnAiGateway } from "../utils/ai_gateway_notice.js";
15
+ import { announceTargetBranch } from "../utils/branch_notice.js";
16
+ import { renderAppliedChanges, renderBranchSettingConflicts } from "../utils/config_diff.js";
19
17
  import { bundleEntry } from "../utils/esbuild.js";
20
- import { addDependenciesArgs, resolvePackageManager, runCommand, } from "../utils/package_manager.js";
21
18
  import { pickServicesInteractively } from "../utils/service_picker.js";
22
19
  import { zipBundle } from "../utils/zip.js";
23
- import { writer } from "../writer.js";
24
20
  import { autoPullEnvAfterPin } from "./env.js";
21
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ import chalk from "chalk";
24
+ import { packagesToStage, resolveConfig } from "@neon/config";
25
+ import { PushConflictError, apply, assertZipWithinLimits, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, loadConfigFromFile as loadConfigFromFile$1, plan, traceNativePackages } from "@neon/config-runtime";
26
+ //#region src/commands/config.ts
27
+ var config_exports = /* @__PURE__ */ __exportAll({
28
+ applyCmd: () => applyCmd,
29
+ applyFlags: () => applyFlags,
30
+ applyPolicyOnCreate: () => applyPolicyOnCreate,
31
+ builder: () => builder,
32
+ command: () => command,
33
+ createBranchFromPolicyOnCheckout: () => createBranchFromPolicyOnCheckout,
34
+ describe: () => describe,
35
+ envFlag: () => envFlag,
36
+ envPullFlag: () => envPullFlag,
37
+ handler: () => handler,
38
+ hasNeonConfigFile: () => hasNeonConfigFile,
39
+ initCmd: () => initCmd,
40
+ planCmd: () => planCmd,
41
+ status: () => status
42
+ });
25
43
  /**
26
- * Bundle a function with neonctl's OWN bundler (the shared esbuild helper) so the
27
- * config-runtime never has to import esbuild itself. Injecting this keeps esbuild
28
- * out of config-runtime's static module graph — and therefore out of the packaged
29
- * neonctl snapshot, which resolves esbuild dynamically at deploy time.
30
- */
31
- const neonctlBundler = async (fn) => zipBundle(await bundleEntry(fn.source, {
32
- ...(fn.externalPackages
33
- ? { externalPackages: fn.externalPackages }
34
- : {}),
35
- }));
36
- const INSPECT_FIELDS = ["project", "branch", "config"];
37
- /**
38
- * Shared `--env` flag for `config plan|apply` and `deploy`. Loads a `.env` into
39
- * `process.env` before the policy is evaluated.
40
- */
41
- export const envFlag = {
42
- env: {
43
- describe: "Path to a .env file to load into the environment before evaluating neon.ts " +
44
- "(so function env values resolve from it). Existing env vars are not overridden.",
45
- type: "string",
46
- },
44
+ * Bundle a function with neonctl's OWN bundler (the shared esbuild helper) so the
45
+ * config-runtime never has to import esbuild itself. Injecting this keeps esbuild
46
+ * out of config-runtime's static module graph — and therefore out of the packaged
47
+ * neonctl snapshot, which resolves esbuild dynamically at deploy time.
48
+ */
49
+ const neonctlBundler = async (fn) => {
50
+ const externalPackages = fn.externalPackages ?? [];
51
+ const { files, metafile, warnings } = await bundleEntry(fn.source, { externalPackages: externalPackages.map((pkg) => pkg.name) });
52
+ for (const warning of warnings) log.warning(warning);
53
+ for (const finding of findUndeclaredNativePackages({
54
+ metafile,
55
+ declared: externalPackages.map((pkg) => pkg.name),
56
+ projectDir: dirname(fn.source)
57
+ })) log.warning(describeNativeFinding(fn.slug, finding));
58
+ const staged = packagesToStage(externalPackages);
59
+ if (staged.length === 0) return zipBundle(files);
60
+ const traced = await traceNativePackages({
61
+ slug: fn.slug,
62
+ packages: staged,
63
+ projectDir: dirname(fn.source)
64
+ });
65
+ for (const warning of traced.warnings) log.warning(warning);
66
+ const entries = {
67
+ ...files,
68
+ ...traced.entries
69
+ };
70
+ enforceLimits(fn.slug, entries);
71
+ const zip = zipBundle(entries);
72
+ assertZipWithinLimits(fn.slug, zip, entries);
73
+ return zip;
47
74
  };
75
+ const INSPECT_FIELDS = [
76
+ "project",
77
+ "branch",
78
+ "config"
79
+ ];
80
+ /**
81
+ * Shared `--env` flag for `config plan|apply` and `deploy`. Loads a `.env` into
82
+ * `process.env` before the policy is evaluated.
83
+ */
84
+ const envFlag = { env: {
85
+ describe: "Path to a .env file to load into the environment before evaluating neon.ts (so function env values resolve from it). Existing env vars are not overridden.",
86
+ type: "string"
87
+ } };
48
88
  /** Apply-only flags, exported so `deploy` can reuse the exact same surface. */
49
- export const applyFlags = {
50
- "update-existing": {
51
- describe: "Auto-confirm overriding existing remote settings on the branch",
52
- type: "boolean",
53
- default: false,
54
- },
55
- "allow-protected": {
56
- describe: "Auto-confirm applying to a branch marked protected on Neon",
57
- type: "boolean",
58
- default: false,
59
- },
89
+ const applyFlags = {
90
+ "update-existing": {
91
+ describe: "Auto-confirm overriding existing remote settings on the branch",
92
+ type: "boolean",
93
+ default: false
94
+ },
95
+ "allow-protected": {
96
+ describe: "Auto-confirm applying to a branch marked protected on Neon",
97
+ type: "boolean",
98
+ default: false
99
+ }
60
100
  };
61
101
  /**
62
- * `--env-pull` for `config apply` / `deploy` (shared so both expose the identical surface).
63
- * After a successful apply, the branch's Neon env vars are written to a local `.env` — the
64
- * same bundled convenience as `link` / `checkout`. On by default; `--no-env-pull` opts out.
65
- */
66
- export const envPullFlag = {
67
- "env-pull": {
68
- describe: "Pull the branch's Neon env vars (DATABASE_URL, …) into a local .env after a " +
69
- "successful apply. On by default; use --no-env-pull to skip (e.g. when injecting " +
70
- "env at runtime with `neon-env run` / `neon dev`).",
71
- type: "boolean",
72
- default: true,
73
- },
74
- };
75
- // ── `config init` ─────────────────────────────────────────────────────────────
102
+ * `--env-pull` for `config apply` / `deploy` (shared so both expose the identical surface).
103
+ * After a successful apply, the branch's Neon env vars are written to a local `.env` — the
104
+ * same bundled convenience as `link` / `checkout`. On by default; `--no-env-pull` opts out.
105
+ */
106
+ const envPullFlag = { "env-pull": {
107
+ describe: "Pull the branch's Neon env vars (DATABASE_URL, …) into a local .env after a successful apply. On by default; use --no-env-pull to skip (e.g. when injecting env at runtime with `neon-env run` / `neon dev`).",
108
+ type: "boolean",
109
+ default: true
110
+ } };
76
111
  /** package.json fields a dependency can be declared in. */
77
112
  const DEPENDENCY_FIELDS = [
78
- "dependencies",
79
- "devDependencies",
80
- "peerDependencies",
81
- "optionalDependencies",
113
+ "dependencies",
114
+ "devDependencies",
115
+ "peerDependencies",
116
+ "optionalDependencies"
82
117
  ];
83
118
  /** Config filenames the runtime loads (mirrors @neon/config's loader). */
84
- const NEON_CONFIG_FILENAMES = ["neon.ts", "neon.mts", "neon.js", "neon.mjs"];
119
+ const NEON_CONFIG_FILENAMES = [
120
+ "neon.ts",
121
+ "neon.mts",
122
+ "neon.js",
123
+ "neon.mjs"
124
+ ];
85
125
  /** Whether `dir` already has a Neon config file the runtime would load. */
86
- export const hasNeonConfigFile = (dir) => NEON_CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
126
+ const hasNeonConfigFile = (dir) => NEON_CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
87
127
  const isRecord = (value) => typeof value === "object" && value !== null;
88
128
  /**
89
- * The {@link REQUIRED_PACKAGES} not already declared in the project's package.json
90
- * (any dependency field). A missing or malformed package.json means none are
91
- * declared, so all are reported missing.
92
- */
129
+ * The {@link REQUIRED_PACKAGES} not already declared in the project's package.json
130
+ * (any dependency field). A missing or malformed package.json means none are
131
+ * declared, so all are reported missing.
132
+ */
93
133
  const missingDependencies = (cwd) => {
94
- const declared = new Set();
95
- const pkgPath = join(cwd, "package.json");
96
- if (existsSync(pkgPath)) {
97
- let parsed;
98
- try {
99
- parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
100
- }
101
- catch {
102
- parsed = undefined;
103
- }
104
- if (isRecord(parsed)) {
105
- for (const field of DEPENDENCY_FIELDS) {
106
- const deps = parsed[field];
107
- if (isRecord(deps)) {
108
- for (const name of Object.keys(deps))
109
- declared.add(name);
110
- }
111
- }
112
- }
113
- }
114
- return REQUIRED_PACKAGES.filter((pkg) => !declared.has(pkg));
134
+ const declared = /* @__PURE__ */ new Set();
135
+ const pkgPath = join(cwd, "package.json");
136
+ if (existsSync(pkgPath)) {
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
140
+ } catch {
141
+ parsed = void 0;
142
+ }
143
+ if (isRecord(parsed)) for (const field of DEPENDENCY_FIELDS) {
144
+ const deps = parsed[field];
145
+ if (isRecord(deps)) for (const name of Object.keys(deps)) declared.add(name);
146
+ }
147
+ }
148
+ return REQUIRED_PACKAGES.filter((pkg) => !declared.has(pkg));
115
149
  };
116
150
  /**
117
- * Which services the scaffolded policy declares. `--services` always wins so a script or an
118
- * agent gets the same result without a TTY; an injected picker is next (tests); the real
119
- * picker only runs on an interactive terminal outside CI. Everything else scaffolds the
120
- * starter policy, which is what `config init` has always written.
121
- */
151
+ * Which services the scaffolded policy declares. `--services` always wins so a script or an
152
+ * agent gets the same result without a TTY; an injected picker is next (tests); the real
153
+ * picker only runs on an interactive terminal outside CI. Everything else scaffolds the
154
+ * starter policy, which is what `config init` has always written.
155
+ */
122
156
  const resolveServices = async (props) => {
123
- if (props.services !== undefined) {
124
- return parseServices(props.services, {
125
- allowed: CONFIG_INIT_SERVICES,
126
- whyUnavailable: CONFIG_INIT_UNAVAILABLE,
127
- flag: "--services",
128
- noneMeans: CONFIG_INIT_NONE_MEANS,
129
- onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical)),
130
- });
131
- }
132
- if (props.pickServices) {
133
- return props.pickServices();
134
- }
135
- if (isCi() || !process.stdout.isTTY) {
136
- return [];
137
- }
138
- return pickServicesInteractively();
157
+ if (props.services !== void 0) return parseServices(props.services, {
158
+ allowed: CONFIG_INIT_SERVICES,
159
+ whyUnavailable: CONFIG_INIT_UNAVAILABLE,
160
+ flag: "--services",
161
+ noneMeans: CONFIG_INIT_NONE_MEANS,
162
+ onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
163
+ });
164
+ if (props.pickServices) return props.pickServices();
165
+ if (isCi() || !process.stdout.isTTY) return [];
166
+ return pickServicesInteractively();
139
167
  };
140
168
  /**
141
- * Write the hello-world handler the scaffolded `preview.functions` entry points at. An
142
- * existing `hello.ts` is left alone: the declared function keeps pointing at it, which is the
143
- * better outcome than overwriting a file the user wrote.
144
- */
169
+ * Write the hello-world handler the scaffolded `preview.functions` entry points at. An
170
+ * existing `hello.ts` is left alone: the declared function keeps pointing at it, which is the
171
+ * better outcome than overwriting a file the user wrote.
172
+ */
145
173
  const scaffoldFunction = (cwd) => {
146
- const path = join(cwd, FUNCTION_FILENAME);
147
- if (existsSync(path)) {
148
- log.info("Found an existing %s — leaving it untouched; the %s function points at it.", FUNCTION_FILENAME, FUNCTION_SLUG);
149
- return;
150
- }
151
- writeFileSync(path, FUNCTION_TEMPLATE);
152
- log.info("Created %s — the source of the %s function.", FUNCTION_FILENAME, FUNCTION_SLUG);
174
+ const path = join(cwd, FUNCTION_FILENAME);
175
+ if (existsSync(path)) {
176
+ log.info("Found an existing %s — leaving it untouched; the %s function points at it.", FUNCTION_FILENAME, FUNCTION_SLUG);
177
+ return;
178
+ }
179
+ writeFileSync(path, FUNCTION_TEMPLATE);
180
+ log.info("Created %s — the source of the %s function.", FUNCTION_FILENAME, FUNCTION_SLUG);
153
181
  };
154
182
  /**
155
- * Read a branch's live state and render it as a `neon.ts`. The read goes through the same
156
- * {@link liveConfigView} as `config status`, so a seeded policy declares exactly what
157
- * `config status --config-json` reports — including what it cannot report (see
158
- * {@link renderNeonConfigFromView}).
159
- */
183
+ * Read a branch's live state and render it as a `neon.ts`. The read goes through the same
184
+ * {@link liveConfigView} as `config status`, so a seeded policy declares exactly what
185
+ * `config status --config-json` reports — including what it cannot report (see
186
+ * {@link renderNeonConfigFromView}).
187
+ */
160
188
  const seedFromBranch = async (props) => {
161
- const { apiClient, projectId } = props;
162
- if (!apiClient || !projectId) {
163
- throw new Error("--from-branch needs a project. Pass --project-id, or run `neon link` to pin one in .neon.");
164
- }
165
- const ref = await resolveBranchRef({
166
- apiClient,
167
- projectId,
168
- ...(props.branch !== undefined ? { branch: props.branch } : {}),
169
- });
170
- if (ref.usedDefault) {
171
- log.info("No branch pinned or passed — seeding from the project's default branch %s.", ref.branchName);
172
- }
173
- const { live, view } = await liveConfigView({
174
- projectId,
175
- branchId: ref.branchId,
176
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
177
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
178
- ...(props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}),
179
- });
180
- const rendered = renderNeonConfigFromView(view, live.branch.name);
181
- return { ...rendered, branchName: live.branch.name };
189
+ const { apiClient, projectId } = props;
190
+ if (!apiClient || !projectId) throw new Error("--from-branch needs a project. Pass --project-id, or run `neon link` to pin one in .neon.");
191
+ const ref = await resolveBranchRef({
192
+ apiClient,
193
+ projectId,
194
+ ...props.branch !== void 0 ? { branch: props.branch } : {}
195
+ });
196
+ if (ref.usedDefault) log.info("No branch pinned or passed — seeding from the project's default branch %s.", ref.branchName);
197
+ const { live, view } = await liveConfigView({
198
+ projectId,
199
+ branchId: ref.branchId,
200
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
201
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
202
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
203
+ });
204
+ return {
205
+ ...renderNeonConfigFromView(view, live.branch.name),
206
+ branchName: live.branch.name
207
+ };
182
208
  };
183
209
  /**
184
- * Scaffold a `neon.ts` policy and make sure the Neon config packages are
185
- * installed, so a project can go straight to `neon config plan` / `apply`.
186
- * Local-only unless `--from-branch` is set (see {@link isConfigInit}).
187
- */
188
- export const initCmd = async (props) => {
189
- const cwd = props.cwd ?? process.cwd();
190
- const run = props.run ?? runCommand;
191
- // 1. Scaffold neon.ts unless the project already has a Neon config file. Resolving the
192
- // services (which may prompt) happens only when there is something to write — asking
193
- // which services to declare and then declaring nothing would be a lie.
194
- const existing = NEON_CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
195
- if (existing) {
196
- log.info("Found an existing %s — leaving it untouched.", existing);
197
- }
198
- else if (props.fromBranch) {
199
- const { source, seeded, branchName } = await seedFromBranch(props);
200
- writeFileSync(join(cwd, "neon.ts"), source);
201
- if (seeded) {
202
- log.info("Created neon.ts from the live state of %s.", branchName);
203
- }
204
- else {
205
- log.info("%s declares no services and no branch settings — created neon.ts with the starter policy instead.", branchName);
206
- }
207
- }
208
- else {
209
- const services = await resolveServices(props);
210
- writeFileSync(join(cwd, "neon.ts"), renderNeonConfig(services));
211
- if (services.length === 0) {
212
- log.info("Created neon.ts with a starter policy.");
213
- }
214
- else {
215
- log.info("Created neon.ts declaring %s.", services.join(", "));
216
- }
217
- if (services.includes("functions")) {
218
- scaffoldFunction(cwd);
219
- }
220
- }
221
- // 2. Make sure the config packages are installed.
222
- const missing = missingDependencies(cwd);
223
- if (missing.length === 0) {
224
- log.info("%s are already installed.", REQUIRED_PACKAGES.join(" and "));
225
- }
226
- else {
227
- const pm = resolvePackageManager(cwd);
228
- const args = addDependenciesArgs(pm, missing);
229
- if (props.install === false) {
230
- log.info("Install the Neon config packages to use neon.ts: %s %s", pm, args.join(" "));
231
- }
232
- else {
233
- log.info("Installing %s with %s…", missing.join(", "), pm);
234
- const ok = await run(pm, args, cwd);
235
- if (!ok) {
236
- log.warning("Could not install the config packages automatically. Run by hand: %s %s", pm, args.join(" "));
237
- }
238
- }
239
- }
240
- log.info("Next: edit neon.ts, then run `neon config plan` to preview and `neon config apply`.");
210
+ * Scaffold a `neon.ts` policy and make sure the Neon config packages are
211
+ * installed, so a project can go straight to `neon config plan` / `apply`.
212
+ * Local-only unless `--from-branch` is set (see {@link isConfigInit}).
213
+ */
214
+ const initCmd = async (props) => {
215
+ const cwd = props.cwd ?? process.cwd();
216
+ const run = props.run ?? runCommand;
217
+ const existing = NEON_CONFIG_FILENAMES.find((name) => existsSync(join(cwd, name)));
218
+ if (existing) log.info("Found an existing %s — leaving it untouched.", existing);
219
+ else if (props.fromBranch) {
220
+ const { source, seeded, branchName } = await seedFromBranch(props);
221
+ writeFileSync(join(cwd, "neon.ts"), source);
222
+ if (seeded) log.info("Created neon.ts from the live state of %s.", branchName);
223
+ else log.info("%s declares no services and no branch settings — created neon.ts with the starter policy instead.", branchName);
224
+ } else {
225
+ const services = await resolveServices(props);
226
+ writeFileSync(join(cwd, "neon.ts"), renderNeonConfig(services));
227
+ if (services.length === 0) log.info("Created neon.ts with a starter policy.");
228
+ else log.info("Created neon.ts declaring %s.", services.join(", "));
229
+ if (services.includes("functions")) scaffoldFunction(cwd);
230
+ }
231
+ const missing = missingDependencies(cwd);
232
+ if (missing.length === 0) log.info("%s are already installed.", REQUIRED_PACKAGES.join(" and "));
233
+ else {
234
+ const pm = resolvePackageManager(cwd);
235
+ const args = installArgs(pm, missing);
236
+ if (props.install === false) log.info("Install the Neon config packages to use neon.ts: %s", formatInstallCommand(pm, missing));
237
+ else {
238
+ log.info("Installing %s with %s…", missing.join(", "), pm);
239
+ if (!await run(pm, args, cwd)) log.warning("Could not install the config packages automatically. Run by hand: %s", formatInstallCommand(pm, missing));
240
+ }
241
+ }
242
+ log.info("Next: edit neon.ts, then run `neon config plan` to preview and `neon config apply`.");
241
243
  };
242
- export const command = "config";
243
- export const describe = "Manage a branch with a neon.ts policy";
244
- export const builder = (argv) => argv
245
- .usage("$0 config <sub-command> [options]")
246
- .options({
247
- "project-id": {
248
- describe: "Project ID",
249
- type: "string",
250
- },
251
- branch: {
252
- describe: "Branch ID or name",
253
- type: "string",
254
- },
255
- })
256
- .middleware(fillSingleProject)
257
- .command("status", "Show the branch's live Neon state", (yargs) => yargs.options({
258
- "config-json": {
259
- describe: "Print only the branch's live config as neon.ts-shaped JSON " +
260
- "(services + branch tuning + preview), to stdout. Useful for " +
261
- "scripting or copying into a neon.ts.",
262
- type: "boolean",
263
- default: false,
264
- },
265
- "current-branch": {
266
- describe: "Print only the linked branch name from the local .neon file " +
267
- "(no network). Exits non-zero when no branch is pinned.",
268
- type: "boolean",
269
- default: false,
270
- },
271
- }), (args) => status(args))
272
- .command("plan", "Show what `config apply` would change (dry run)", (yargs) => yargs.options({
273
- config: {
274
- describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
275
- type: "string",
276
- },
277
- ...envFlag,
278
- }), (args) => planCmd(args))
279
- .command("apply", "Apply a neon.ts policy to the branch", (yargs) => yargs.options({
280
- config: {
281
- describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
282
- type: "string",
283
- },
284
- ...envFlag,
285
- ...applyFlags,
286
- ...envPullFlag,
287
- }), (args) => applyCmd(args))
288
- .command("init", "Scaffold a neon.ts policy and install the Neon config packages", (yargs) => yargs.options({
289
- install: {
290
- describe: "Install @neon/config and @neon/env if they're missing. " +
291
- "On by default; use --no-install to just print the command.",
292
- type: "boolean",
293
- default: true,
294
- },
295
- services: servicesOption({
296
- key: "services",
297
- allowed: CONFIG_INIT_SERVICES,
298
- noneMeans: CONFIG_INIT_NONE_MEANS,
299
- describe: "Services the scaffolded neon.ts declares",
300
- also: "Omitted: pick interactively on a terminal, starter policy in " +
301
- "CI or without a TTY.",
302
- }),
303
- "from-branch": {
304
- describe: "Seed neon.ts from a branch's live Neon state instead of asking. Uses the " +
305
- "branch pinned in .neon, or --branch <name|id>, or the project's default " +
306
- "branch. The only mode of `config init` that calls the Neon API.",
307
- type: "boolean",
308
- // No `default`: yargs counts a defaulted key as provided, so
309
- // `default: false` makes `conflicts` reject every `--services` run.
310
- conflicts: "services",
311
- },
244
+ const command = "config";
245
+ const describe = "Manage a branch with a neon.ts policy";
246
+ const builder = (argv) => argv.usage("$0 config <sub-command> [options]").options({
247
+ "project-id": {
248
+ describe: "Project ID",
249
+ type: "string"
250
+ },
251
+ branch: {
252
+ describe: "Branch ID or name",
253
+ type: "string"
254
+ }
255
+ }).middleware(fillSingleProject).command("status", "Show the branch's live Neon state", (yargs) => yargs.options({
256
+ "config-json": {
257
+ describe: "Print only the branch's live config as neon.ts-shaped JSON (services + branch tuning + preview), to stdout. Useful for scripting or copying into a neon.ts.",
258
+ type: "boolean",
259
+ default: false
260
+ },
261
+ "current-branch": {
262
+ describe: "Print only the linked branch name from the local .neon file (no network). Exits non-zero when no branch is pinned.",
263
+ type: "boolean",
264
+ default: false
265
+ }
266
+ }), (args) => status(args)).command("plan", "Show what `config apply` would change (dry run)", (yargs) => yargs.options({
267
+ config: {
268
+ describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
269
+ type: "string"
270
+ },
271
+ ...envFlag
272
+ }), (args) => planCmd(args)).command("apply", "Apply a neon.ts policy to the branch", (yargs) => yargs.options({
273
+ config: {
274
+ describe: "Path to a neon.ts policy (defaults to walking up from cwd)",
275
+ type: "string"
276
+ },
277
+ ...envFlag,
278
+ ...applyFlags,
279
+ ...envPullFlag
280
+ }), (args) => applyCmd(args)).command("init", "Scaffold a neon.ts policy and install the Neon config packages", (yargs) => yargs.options({
281
+ install: {
282
+ describe: "Install @neon/config and @neon/env if they're missing. On by default; use --no-install to just print the command.",
283
+ type: "boolean",
284
+ default: true
285
+ },
286
+ services: servicesOption({
287
+ key: "services",
288
+ allowed: CONFIG_INIT_SERVICES,
289
+ noneMeans: CONFIG_INIT_NONE_MEANS,
290
+ describe: "Services the scaffolded neon.ts declares",
291
+ also: "Omitted: pick interactively on a terminal, starter policy in CI or without a TTY."
292
+ }),
293
+ "from-branch": {
294
+ describe: "Seed neon.ts from a branch's live Neon state instead of asking. Uses the branch pinned in .neon, or --branch <name|id>, or the project's default branch. The only mode of `config init` that calls the Neon API.",
295
+ type: "boolean",
296
+ conflicts: "services"
297
+ }
312
298
  }), (args) => initCmd({
313
- ...args,
314
- // `args` is untyped here, and "flag omitted" has to stay distinct from
315
- // "flag given" — it decides whether the picker runs at all.
316
- services: servicesFlagValue(args.services),
299
+ ...args,
300
+ services: servicesFlagValue(args.services)
317
301
  }));
318
- export const handler = (args) => {
319
- return args;
302
+ const handler = (args) => {
303
+ return args;
320
304
  };
321
305
  /**
322
- * A branch's live state, plus that state projected into the `neon.ts`-shaped
323
- * {@link NeonConfigView}. Shared by `config status` and `config init --from-branch` so both
324
- * read the branch through one path: what `status --config-json` prints is exactly what
325
- * `init --from-branch` writes.
326
- *
327
- * The pulled `config` carries the branch's tuning inside a closure that JSON can't render, so
328
- * it is resolved against the live branch target first.
329
- */
306
+ * A branch's live state, plus that state projected into the `neon.ts`-shaped
307
+ * {@link NeonConfigView}. Shared by `config status` and `config init --from-branch` so both
308
+ * read the branch through one path: what `status --config-json` prints is exactly what
309
+ * `init --from-branch` writes.
310
+ *
311
+ * The pulled `config` carries the branch's tuning inside a closure that JSON can't render, so
312
+ * it is resolved against the live branch target first.
313
+ */
330
314
  const liveConfigView = async (opts) => {
331
- const live = await inspect({
332
- projectId: opts.projectId,
333
- branchId: opts.branchId,
334
- ...(opts.apiKey ? { apiKey: opts.apiKey } : {}),
335
- ...(opts.apiHost ? { apiHost: opts.apiHost } : {}),
336
- ...(opts.runtimeApi ? { api: opts.runtimeApi } : {}),
337
- });
338
- const resolved = resolveConfig(live.config, {
339
- name: live.branch.name,
340
- id: live.branch.id,
341
- exists: true,
342
- isDefault: live.branch.isDefault,
343
- isProtected: live.branch.protected,
344
- ...(live.branch.parent ? { parentId: live.branch.parent } : {}),
345
- ...(live.branch.expiresAt ? { expiresAt: live.branch.expiresAt } : {}),
346
- });
347
- return { live, view: toNeonConfigView(resolved, live.preview) };
315
+ const live = await inspect({
316
+ projectId: opts.projectId,
317
+ branchId: opts.branchId,
318
+ ...opts.apiKey ? { apiKey: opts.apiKey } : {},
319
+ ...opts.apiHost ? { apiHost: opts.apiHost } : {},
320
+ ...opts.runtimeApi ? { api: opts.runtimeApi } : {}
321
+ });
322
+ const resolved = resolveConfig(live.config, {
323
+ name: live.branch.name,
324
+ id: live.branch.id,
325
+ exists: true,
326
+ isDefault: live.branch.isDefault,
327
+ isProtected: live.branch.protected,
328
+ ...live.branch.parent ? { parentId: live.branch.parent } : {},
329
+ ...live.branch.expiresAt ? { expiresAt: live.branch.expiresAt } : {}
330
+ });
331
+ return {
332
+ live,
333
+ view: toNeonConfigView(resolved, live.preview)
334
+ };
348
335
  };
349
336
  const loadConfig = async (props) => {
350
- // Load the optional --env file FIRST so a `neon.ts` whose function `env` values read
351
- // `process.env.X` sees them. Must happen before the policy module is imported/evaluated.
352
- if (props.env) {
353
- const applied = loadEnvFileIntoProcess(props.env);
354
- log.debug("Loaded %d var(s) from %s into the environment: %s", applied.length, props.env, applied.join(", "));
355
- }
356
- const { config } = await loadConfigFromFile({
357
- ...(props.config ? { path: props.config } : {}),
358
- });
359
- return config;
337
+ if (props.env) {
338
+ const applied = loadEnvFileIntoProcess(props.env);
339
+ log.debug("Loaded %d var(s) from %s into the environment: %s", applied.length, props.env, applied.join(", "));
340
+ }
341
+ const { config } = await loadConfigFromFile$1({ ...props.config ? { path: props.config } : {} });
342
+ return config;
360
343
  };
361
- export const status = async (props) => {
362
- // `--current-branch` short-circuits here (before resolveBranchRef), so it wins
363
- // over --config-json and ignores --output. See ConfigProps.currentBranch / isCurrentBranchProbe.
364
- if (props.currentBranch) {
365
- const branch = contextBranch(readContextFile(props.contextFile));
366
- if (branch) {
367
- process.stdout.write(`${branch}\n`);
368
- }
369
- else {
370
- // No branch pinned: hint on stderr and exit non-zero (grep-style) so a prompt's
371
- // `when` hides the segment cleanly instead of rendering a bare icon.
372
- log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
373
- process.exitCode = 1;
374
- }
375
- return;
376
- }
377
- const branch = await resolveBranchRef(props);
378
- // `--config-json` is a script-friendly mode that emits only JSON to stdout, so keep it
379
- // pristine; the regular human view gets the "which branch am I inspecting" guardrail.
380
- if (!props.configJson) {
381
- announceTargetBranch(props, branch, "Inspecting branch");
382
- }
383
- const { live, view: configView } = await liveConfigView({
384
- projectId: props.projectId,
385
- branchId: branch.branchId,
386
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
387
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
388
- ...(props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}),
389
- });
390
- // `--config-json`: emit just the neon.ts-shaped config to stdout (script-friendly,
391
- // copy-paste-able), regardless of the global --output.
392
- if (props.configJson) {
393
- process.stdout.write(`${JSON.stringify(configView, null, 2)}\n`);
394
- return;
395
- }
396
- // Default: the live project/branch tables, but with the unhelpful raw `config` replaced
397
- // by the resolved neon.ts-shaped view so the user sees enabled infra + branch tuning.
398
- writer(props).end({ project: live.project, branch: live.branch, config: configView }, { fields: INSPECT_FIELDS });
344
+ const status = async (props) => {
345
+ if (props.currentBranch) {
346
+ const branch = contextBranch(readContextFile(props.contextFile));
347
+ if (branch) process.stdout.write(`${branch}\n`);
348
+ else {
349
+ log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
350
+ process.exitCode = 1;
351
+ }
352
+ return;
353
+ }
354
+ const branch = await resolveBranchRef(props);
355
+ if (!props.configJson) announceTargetBranch(props, branch, "Inspecting branch");
356
+ const { live, view: configView } = await liveConfigView({
357
+ projectId: props.projectId,
358
+ branchId: branch.branchId,
359
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
360
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
361
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
362
+ });
363
+ if (props.configJson) {
364
+ process.stdout.write(`${JSON.stringify(configView, null, 2)}\n`);
365
+ return;
366
+ }
367
+ writer(props).end({
368
+ project: live.project,
369
+ branch: live.branch,
370
+ config: configView
371
+ }, { fields: INSPECT_FIELDS });
399
372
  };
400
- export const planCmd = async (props) => {
401
- const config = await loadConfig(props);
402
- const branch = await resolveBranchRef(props);
403
- announceTargetBranch(props, branch, "Planning against branch");
404
- const branchId = branch.branchId;
405
- // `plan` is a dry run that never bundles, so its options don't accept (or need)
406
- // an injected bundler — only `apply` does (it uses neonctlBundler).
407
- const result = await plan(config, {
408
- projectId: props.projectId,
409
- branchId,
410
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
411
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
412
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
413
- });
414
- const services = utilizedServices(config);
415
- reportPushResult(props, result, "plan", services);
416
- // `plan` is a dry run and never pulls credentials, so it can only offer the plan-based
417
- // (Free) AI Gateway notice — the reduced-model-set check needs a live gateway token,
418
- // which `apply`/`checkout`/`env pull` get via the bundled env pull. Best-effort.
419
- if (services.includes("AI Gateway")) {
420
- await warnAiGateway({
421
- apiClient: props.apiClient,
422
- projectId: props.projectId,
423
- branchId,
424
- });
425
- }
373
+ const planCmd = async (props) => {
374
+ const config = await loadConfig(props);
375
+ const branch = await resolveBranchRef(props);
376
+ announceTargetBranch(props, branch, "Planning against branch");
377
+ const branchId = branch.branchId;
378
+ const result = await plan(config, {
379
+ projectId: props.projectId,
380
+ branchId,
381
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
382
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
383
+ ...props.runtimeApi ? { api: props.runtimeApi } : {}
384
+ });
385
+ const services = utilizedServices(config);
386
+ reportPushResult(props, result, "plan", services);
387
+ if (services.includes("AI Gateway")) await warnAiGateway({
388
+ apiClient: props.apiClient,
389
+ projectId: props.projectId,
390
+ branchId
391
+ });
426
392
  };
427
- export const applyCmd = async (props) => {
428
- const config = await loadConfig(props);
429
- const branch = await resolveBranchRef(props);
430
- announceTargetBranch(props, branch, "Applying to branch");
431
- const branchId = branch.branchId;
432
- // The AI Gateway can't serve on the Free plan, so refuse to provision it up front rather
433
- // than write a credential that won't work. Only when the policy actually enables the
434
- // gateway; best-effort on the plan lookup (a transient failure never blocks a paid user).
435
- if (utilizedServices(config).includes("AI Gateway")) {
436
- await assertAiGatewayProvisionable({
437
- apiClient: props.apiClient,
438
- projectId: props.projectId,
439
- });
440
- }
441
- let result;
442
- try {
443
- result = await apply(config, {
444
- projectId: props.projectId,
445
- branchId,
446
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
447
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
448
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
449
- ...(props.updateExisting ? { updateExisting: true } : {}),
450
- ...(props.allowProtected ? { allowProtectedBranch: true } : {}),
451
- bundleFunction: neonctlBundler,
452
- });
453
- }
454
- catch (err) {
455
- // Drift without `--update-existing` throws with the conflicting fields attached.
456
- // Render them as the same git-style before→after diff, then fail with a concise
457
- // message (the detailed diff above replaces the library's long multi-line text).
458
- if (err instanceof PushConflictError) {
459
- reportConflicts(props, err.conflicts);
460
- throw new Error("Branch settings conflict with the policy. Re-run with --update-existing to apply the changes shown above.");
461
- }
462
- throw err;
463
- }
464
- reportPushResult(props, result, "apply", utilizedServices(config));
465
- // After a successful apply/deploy, write the branch's Neon env vars to a local .env —
466
- // the same bundled convenience as `link` / `checkout`, so the branch is immediately
467
- // usable for local dev. `--no-env-pull` opts out; a pull failure degrades to a warning
468
- // (the apply already succeeded). See autoPullEnvAfterPin.
469
- await autoPullEnvAfterPin({ ...props, envPull: props.envPull !== false });
393
+ const applyCmd = async (props) => {
394
+ const config = await loadConfig(props);
395
+ const branch = await resolveBranchRef(props);
396
+ announceTargetBranch(props, branch, "Applying to branch");
397
+ const branchId = branch.branchId;
398
+ if (utilizedServices(config).includes("AI Gateway")) await assertAiGatewayProvisionable({
399
+ apiClient: props.apiClient,
400
+ projectId: props.projectId
401
+ });
402
+ let result;
403
+ try {
404
+ result = await apply(config, {
405
+ projectId: props.projectId,
406
+ branchId,
407
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
408
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
409
+ ...props.runtimeApi ? { api: props.runtimeApi } : {},
410
+ ...props.updateExisting ? { updateExisting: true } : {},
411
+ ...props.allowProtected ? { allowProtectedBranch: true } : {},
412
+ bundleFunction: neonctlBundler
413
+ });
414
+ } catch (err) {
415
+ if (err instanceof PushConflictError) {
416
+ reportConflicts(props, err.conflicts);
417
+ throw new Error("Branch settings conflict with the policy. Re-run with --update-existing to apply the changes shown above.");
418
+ }
419
+ throw err;
420
+ }
421
+ reportPushResult(props, result, "apply", utilizedServices(config));
422
+ await autoPullEnvAfterPin({
423
+ ...props,
424
+ envPull: props.envPull !== false
425
+ });
470
426
  };
471
427
  /**
472
- * A static service toggle (`auth` / `dataApi` / `preview.aiGateway`) is "on" unless
473
- * explicitly disabled: `true` / `{}` / `{ enabled: true }` enable it; `false` /
474
- * `{ enabled: false }` / absent leave it off. Mirrors the runtime's `isServiceEnabled`
475
- * (which isn't exported), kept tiny and pure so it can be read straight off the policy.
476
- */
428
+ * A static service toggle (`auth` / `dataApi` / `preview.aiGateway`) is "on" unless
429
+ * explicitly disabled: `true` / `{}` / `{ enabled: true }` enable it; `false` /
430
+ * `{ enabled: false }` / absent leave it off. Mirrors the runtime's `isServiceEnabled`
431
+ * (which isn't exported), kept tiny and pure so it can be read straight off the policy.
432
+ */
477
433
  const isToggleEnabled = (toggle) => {
478
- if (toggle === undefined)
479
- return false;
480
- if (typeof toggle === "boolean")
481
- return toggle;
482
- return toggle.enabled !== false;
434
+ if (toggle === void 0) return false;
435
+ if (typeof toggle === "boolean") return toggle;
436
+ return toggle.enabled !== false;
483
437
  };
484
438
  /**
485
- * Human-readable list of the services a `neon.ts` policy utilizes on the branch, shown under
486
- * the plan/apply table. Postgres is always present (every branch has it); the rest are listed
487
- * only when the policy declares them. This deliberately surfaces services that produce **no**
488
- * plan step — notably the AI Gateway, which is always available and only needs a scoped branch
489
- * credential (not a provisioning step) — so adding `preview.aiGateway` to a neon.ts isn't
490
- * mistaken for being silently dropped. Service enablement is static top-level config (it never
491
- * lives in the per-branch closure), so reading it straight off `config` is accurate.
492
- */
439
+ * Human-readable list of the services a `neon.ts` policy utilizes on the branch, shown under
440
+ * the plan/apply table. Postgres is always present (every branch has it); the rest are listed
441
+ * only when the policy declares them. This deliberately surfaces services that produce **no**
442
+ * plan step — notably the AI Gateway, which is always available and only needs a scoped branch
443
+ * credential (not a provisioning step) — so adding `preview.aiGateway` to a neon.ts isn't
444
+ * mistaken for being silently dropped. Service enablement is static top-level config (it never
445
+ * lives in the per-branch closure), so reading it straight off `config` is accurate.
446
+ */
493
447
  const utilizedServices = (config) => {
494
- const services = ["Postgres"];
495
- if (isToggleEnabled(config.auth))
496
- services.push("Neon Auth");
497
- if (isToggleEnabled(config.dataApi))
498
- services.push("Data API");
499
- if (Object.keys(config.preview?.buckets ?? {}).length > 0) {
500
- services.push("Object Storage");
501
- }
502
- if (Object.keys(config.preview?.functions ?? {}).length > 0) {
503
- services.push("Functions");
504
- }
505
- if (isToggleEnabled(config.preview?.aiGateway))
506
- services.push("AI Gateway");
507
- return services;
448
+ const services = ["Postgres"];
449
+ if (isToggleEnabled(config.auth)) services.push("Neon Auth");
450
+ if (isToggleEnabled(config.dataApi)) services.push("Data API");
451
+ if (Object.keys(config.preview?.buckets ?? {}).length > 0) services.push("Object Storage");
452
+ if (Object.keys(config.preview?.functions ?? {}).length > 0) services.push("Functions");
453
+ if (isToggleEnabled(config.preview?.aiGateway)) services.push("AI Gateway");
454
+ return services;
508
455
  };
509
456
  /**
510
- * Render a {@link PushResult}. JSON/YAML output emits the raw result (plus a `services`
511
- * summary) verbatim so it can be piped; the human-readable path renders the actual changes
512
- * (dropping noops) and any blocking conflicts as a `git diff`-style report, or a "nothing to
513
- * do" line when both are empty — and always closes with the list of services the policy
514
- * utilizes so a service that produces no plan step (Postgres, or the credential-gated AI
515
- * Gateway) isn't mistaken for being missing from the plan above.
516
- *
517
- * The diff is asymmetric on purpose (see the CLI's `neon diff`): **service** changes are
518
- * additions with no "before", so they list as `+`/`~` lines; **branch setting** changes have
519
- * a natural before→after, so conflicts render as a sorted `current → desired` diff. Planned
520
- * branch updates (under `--update-existing`) carry only the new value, so they render
521
- * desired-only for now (the previous value isn't threaded through the runtime yet).
522
- */
457
+ * Render a {@link PushResult}. JSON/YAML output emits the raw result (plus a `services`
458
+ * summary) verbatim so it can be piped; the human-readable path renders the actual changes
459
+ * (dropping noops) and any blocking conflicts as a `git diff`-style report, or a "nothing to
460
+ * do" line when both are empty — and always closes with the list of services the policy
461
+ * utilizes so a service that produces no plan step (Postgres, or the credential-gated AI
462
+ * Gateway) isn't mistaken for being missing from the plan above.
463
+ *
464
+ * The diff is asymmetric on purpose (see the CLI's `neon diff`): **service** changes are
465
+ * additions with no "before", so they list as `+`/`~` lines; **branch setting** changes have
466
+ * a natural before→after, so conflicts render as a sorted `current → desired` diff. Planned
467
+ * branch updates (under `--update-existing`) carry only the new value, so they render
468
+ * desired-only for now (the previous value isn't threaded through the runtime yet).
469
+ */
523
470
  const reportPushResult = (props, result, mode, services) => {
524
- if (props.output === "json" || props.output === "yaml") {
525
- writer(props).end({ ...result, services }, { fields: [] });
526
- return;
527
- }
528
- const appliedChanges = result.applied.filter((change) => change.action !== "noop");
529
- // Deployed functions carry their invocation URL in the change details — collect them so
530
- // we can list where to call each function without digging through the raw details blob.
531
- // Keyed by slug so a function never shows twice.
532
- const functionUrlBySlug = new Map();
533
- for (const change of appliedChanges) {
534
- const slug = change.details?.slug;
535
- const invocationUrl = change.details?.invocationUrl;
536
- if (typeof slug === "string" && typeof invocationUrl === "string") {
537
- functionUrlBySlug.set(slug, invocationUrl);
538
- }
539
- }
540
- // chalk self-detects TTY/NO_COLOR; `--no-color` (props.color === false) forces plain.
541
- const color = props.color !== false;
542
- const out = writer(props);
543
- // Conflicts never reach here in the CLI: `plan` runs with updateExisting on, and a bare
544
- // `apply` throws PushConflictError (rendered by reportConflicts). So an empty applied set
545
- // is the whole story here.
546
- const noChanges = appliedChanges.length === 0;
547
- const appliedText = renderAppliedChanges(appliedChanges, mode === "plan" ? "Planned changes" : "Applied changes", { color });
548
- if (appliedText)
549
- out.text(`${appliedText}\n`);
550
- // Function URLs are a plain list rather than a table: an invocation URL can be 70+ chars,
551
- // which makes any bordered table overflow and wrap awkwardly in a normal terminal. A list
552
- // lets each URL reflow on its own line, and stays copy-pasteable.
553
- if (functionUrlBySlug.size > 0) {
554
- const heading = mode === "plan" ? "Function URLs (after apply)" : "Function URLs";
555
- out.text(`\n${isCi() ? heading : chalk.bold(heading)}\n`);
556
- for (const [slug, invocationUrl] of functionUrlBySlug) {
557
- out.text(` • ${slug}: ${invocationUrl}\n`);
558
- }
559
- }
560
- if (noChanges) {
561
- log.info(`No changes — branch ${result.branchName} already matches the policy.`);
562
- }
563
- out.text(`\nUtilized services: ${services.join(", ")}\n`);
471
+ if (props.output === "json" || props.output === "yaml") {
472
+ writer(props).end({
473
+ ...result,
474
+ services
475
+ }, { fields: [] });
476
+ return;
477
+ }
478
+ const appliedChanges = result.applied.filter((change) => change.action !== "noop");
479
+ const functionUrlBySlug = /* @__PURE__ */ new Map();
480
+ for (const change of appliedChanges) {
481
+ const slug = change.details?.slug;
482
+ const invocationUrl = change.details?.invocationUrl;
483
+ if (typeof slug === "string" && typeof invocationUrl === "string") functionUrlBySlug.set(slug, invocationUrl);
484
+ }
485
+ const color = props.color !== false;
486
+ const out = writer(props);
487
+ const noChanges = appliedChanges.length === 0;
488
+ const appliedText = renderAppliedChanges(appliedChanges, mode === "plan" ? "Planned changes" : "Applied changes", { color });
489
+ if (appliedText) out.text(`${appliedText}\n`);
490
+ if (functionUrlBySlug.size > 0) {
491
+ const heading = mode === "plan" ? "Function URLs (after apply)" : "Function URLs";
492
+ out.text(`\n${isCi() ? heading : chalk.bold(heading)}\n`);
493
+ for (const [slug, invocationUrl] of functionUrlBySlug) out.text(` • ${slug}: ${invocationUrl}\n`);
494
+ }
495
+ if (noChanges) log.info(`No changes — branch ${result.branchName} already matches the policy.`);
496
+ out.text(`\nUtilized services: ${services.join(", ")}\n`);
564
497
  };
565
498
  /**
566
- * Render the branch-setting {@link ConflictReport}s a bare `apply` refused to override (drift
567
- * without `--update-existing`) as the git-style before→after diff. JSON/YAML output emits the
568
- * structured conflicts so it can be piped; the human path prints the sorted diff followed by
569
- * any conflict whose fix is *not* `--update-existing` (e.g. an immutable "no endpoint" case),
570
- * so nothing the library's error message carried is lost.
571
- */
499
+ * Render the branch-setting {@link ConflictReport}s a bare `apply` refused to override (drift
500
+ * without `--update-existing`) as the git-style before→after diff. JSON/YAML output emits the
501
+ * structured conflicts so it can be piped; the human path prints the sorted diff followed by
502
+ * any conflict whose fix is *not* `--update-existing` (e.g. an immutable "no endpoint" case),
503
+ * so nothing the library's error message carried is lost.
504
+ */
572
505
  const reportConflicts = (props, conflicts) => {
573
- if (props.output === "json" || props.output === "yaml") {
574
- writer(props).end({ conflicts }, { fields: [] });
575
- return;
576
- }
577
- const out = writer(props);
578
- const text = renderBranchSettingConflicts([...conflicts], {
579
- color: props.color !== false,
580
- });
581
- if (text)
582
- out.text(`${text}\n`);
583
- for (const conflict of conflicts) {
584
- if (!/updateExisting/i.test(conflict.reason)) {
585
- out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
586
- }
587
- }
506
+ if (props.output === "json" || props.output === "yaml") {
507
+ writer(props).end({ conflicts }, { fields: [] });
508
+ return;
509
+ }
510
+ const out = writer(props);
511
+ const text = renderBranchSettingConflicts([...conflicts], { color: props.color !== false });
512
+ if (text) out.text(`${text}\n`);
513
+ for (const conflict of conflicts) if (!/updateExisting/i.test(conflict.reason)) out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
588
514
  };
589
515
  /**
590
- * Block provisioning the AI Gateway on a Free plan from the `checkout` policy paths, which
591
- * carry raw credentials (`apiKey`/`apiHost`) rather than the CLI's api client. Builds a client
592
- * from them and defers to {@link assertAiGatewayProvisionable}. Skipped when a `runtimeApi` is
593
- * injected (tests) or no `apiKey` is available; the interactive commands always have a key.
594
- */
516
+ * Block provisioning the AI Gateway on a Free plan from the `checkout` policy paths, which
517
+ * carry raw credentials (`apiKey`/`apiHost`) rather than the CLI's api client. Builds a client
518
+ * from them and defers to {@link assertAiGatewayProvisionable}. Skipped when a `runtimeApi` is
519
+ * injected (tests) or no `apiKey` is available; the interactive commands always have a key.
520
+ */
595
521
  const assertAiGatewayProvisionableFromCreds = async (props) => {
596
- if (props.runtimeApi || !props.apiKey)
597
- return;
598
- if (!utilizedServices(props.config).includes("AI Gateway"))
599
- return;
600
- const apiClient = getApiClient({
601
- apiKey: props.apiKey,
602
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
603
- });
604
- await assertAiGatewayProvisionable({
605
- apiClient,
606
- projectId: props.projectId,
607
- });
522
+ if (props.runtimeApi || !props.apiKey) return;
523
+ if (!utilizedServices(props.config).includes("AI Gateway")) return;
524
+ const apiClient = getApiClient({
525
+ apiKey: props.apiKey,
526
+ ...props.apiHost ? { apiHost: props.apiHost } : {}
527
+ });
528
+ await assertAiGatewayProvisionable({
529
+ apiClient,
530
+ projectId: props.projectId
531
+ });
608
532
  };
609
533
  /**
610
- * Apply a `neon.ts` policy to a **freshly created** branch (used by `neonctl checkout`
611
- * when it creates a branch). No-op when there is no `neon.ts` on the path from cwd up to
612
- * the repo root — checkout still succeeds, it just has no policy to apply.
613
- *
614
- * The branch was just created by us, so we apply non-interactively (`updateExisting` /
615
- * `allowProtectedBranch`) — there is no pre-existing state a user would be surprised to
616
- * see overridden. Functions are bundled with neonctl's own esbuild helper.
617
- */
618
- export const applyPolicyOnCreate = async (props) => {
619
- let config;
620
- try {
621
- ({ config } = await loadConfigFromFile({
622
- ...(props.cwd ? { cwd: props.cwd } : {}),
623
- }));
624
- }
625
- catch (err) {
626
- const message = err instanceof Error ? err.message : String(err);
627
- if (/Could not find a Neon config file/i.test(message))
628
- return;
629
- throw err;
630
- }
631
- await assertAiGatewayProvisionableFromCreds({
632
- projectId: props.projectId,
633
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
634
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
635
- ...(props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}),
636
- config,
637
- });
638
- log.info("Applying neon.ts policy to the new branch…");
639
- const result = await apply(config, {
640
- projectId: props.projectId,
641
- branchId: props.branchId,
642
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
643
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
644
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
645
- updateExisting: true,
646
- allowProtectedBranch: true,
647
- bundleFunction: neonctlBundler,
648
- });
649
- logPolicyResult(result, { color: props.color !== false });
534
+ * Apply a `neon.ts` policy to a **freshly created** branch (used by `neonctl checkout`
535
+ * when it creates a branch). No-op when there is no `neon.ts` on the path from cwd up to
536
+ * the repo root — checkout still succeeds, it just has no policy to apply.
537
+ *
538
+ * The branch was just created by us, so we apply non-interactively (`updateExisting` /
539
+ * `allowProtectedBranch`) — there is no pre-existing state a user would be surprised to
540
+ * see overridden. Functions are bundled with neonctl's own esbuild helper.
541
+ */
542
+ const applyPolicyOnCreate = async (props) => {
543
+ let config;
544
+ try {
545
+ ({config} = await loadConfigFromFile$1({ ...props.cwd ? { cwd: props.cwd } : {} }));
546
+ } catch (err) {
547
+ const message = err instanceof Error ? err.message : String(err);
548
+ if (/Could not find a Neon config file/i.test(message)) return;
549
+ throw err;
550
+ }
551
+ await assertAiGatewayProvisionableFromCreds({
552
+ projectId: props.projectId,
553
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
554
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
555
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {},
556
+ config
557
+ });
558
+ log.info("Applying neon.ts policy to the new branch…");
559
+ const result = await apply(config, {
560
+ projectId: props.projectId,
561
+ branchId: props.branchId,
562
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
563
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
564
+ ...props.runtimeApi ? { api: props.runtimeApi } : {},
565
+ updateExisting: true,
566
+ allowProtectedBranch: true,
567
+ bundleFunction: neonctlBundler
568
+ });
569
+ logPolicyResult(result, { color: props.color !== false });
650
570
  };
651
571
  /**
652
- * Report what applying a `neon.ts` policy changed, using the same `field → value` diff
653
- * `deploy` prints (see {@link renderAppliedChanges}) rather than a bare list of change
654
- * identifiers — the identifier alone repeats the branch name once per change and never says
655
- * *what* was applied, which is the only interesting part on a freshly created branch.
656
- */
572
+ * Report what applying a `neon.ts` policy changed, using the same `field → value` diff
573
+ * `deploy` prints (see {@link renderAppliedChanges}) rather than a bare list of change
574
+ * identifiers — the identifier alone repeats the branch name once per change and never says
575
+ * *what* was applied, which is the only interesting part on a freshly created branch.
576
+ */
657
577
  const logPolicyResult = (result, opts) => {
658
- const changes = result.applied.filter((c) => c.action !== "noop");
659
- if (changes.length === 0) {
660
- log.info("neon.ts applied — no changes were needed.");
661
- return;
662
- }
663
- log.info("%s", renderAppliedChanges(changes, `neon.ts applied — ${changes.length} change${changes.length === 1 ? "" : "s"}:`, opts));
578
+ const changes = result.applied.filter((c) => c.action !== "noop");
579
+ if (changes.length === 0) {
580
+ log.info("neon.ts applied — no changes were needed.");
581
+ return;
582
+ }
583
+ log.info("%s", renderAppliedChanges(changes, `neon.ts applied — ${changes.length} change${changes.length === 1 ? "" : "s"}:`, opts));
664
584
  };
665
585
  /**
666
- * Create a branch **from** the local `neon.ts` policy. Returns `null` when there is no
667
- * `neon.ts` on the path from cwd up to the repo root, so `neonctl checkout` can fall back to a
668
- * bare branch create.
669
- *
670
- * Unlike a bare create followed by {@link applyPolicyOnCreate}, this evaluates the policy for
671
- * the **new** branch (`exists: false`): the runtime branches from the policy's `parent` and
672
- * brings the branch up with its declared TTL / compute settings / services. That's what makes
673
- * a policy keyed on `!branch.exists` (the common "only configure new branches" shape) take
674
- * effect on the very first `checkout` — a bare create + `apply` always saw `exists: true` and
675
- * skipped that block.
676
- *
677
- * A branch that was created but whose policy failed to apply is reported through
678
- * `policyFailure` rather than thrown: the branch is real, so `checkout` still needs to pin it
679
- * (see the handler) instead of leaving it stranded behind an unchanged `.neon`.
680
- */
681
- export const createBranchFromPolicyOnCheckout = async (props) => {
682
- let config;
683
- try {
684
- ({ config } = await loadConfigFromFile({
685
- ...(props.cwd ? { cwd: props.cwd } : {}),
686
- }));
687
- }
688
- catch (err) {
689
- const message = err instanceof Error ? err.message : String(err);
690
- if (/Could not find a Neon config file/i.test(message))
691
- return null;
692
- throw err;
693
- }
694
- await assertAiGatewayProvisionableFromCreds({
695
- projectId: props.projectId,
696
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
697
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
698
- ...(props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}),
699
- config,
700
- });
701
- try {
702
- const { branchId, branchName, result } = await createBranchFromPolicy(config, {
703
- projectId: props.projectId,
704
- branchName: props.branchName,
705
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
706
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
707
- ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
708
- bundleFunction: neonctlBundler,
709
- });
710
- log.info("Created branch %s (%s) from neon.ts policy.", branchName, branchId);
711
- logPolicyResult(result, { color: props.color !== false });
712
- return { branchId };
713
- }
714
- catch (err) {
715
- // The branch exists but its policy didn't fully apply. Hand the id back so checkout
716
- // pins it and reports the failure with the remediation, rather than aborting with an
717
- // unpinned context and a branch the next `checkout` would silently accept as-is.
718
- if (isPartialBranchCreateError(err)) {
719
- log.info("Created branch %s (%s) from neon.ts policy.", err.branchName, err.branchId);
720
- return { branchId: err.branchId, policyFailure: err.reason };
721
- }
722
- throw err;
723
- }
586
+ * Create a branch **from** the local `neon.ts` policy. Returns `null` when there is no
587
+ * `neon.ts` on the path from cwd up to the repo root, so `neonctl checkout` can fall back to a
588
+ * bare branch create.
589
+ *
590
+ * Unlike a bare create followed by {@link applyPolicyOnCreate}, this evaluates the policy for
591
+ * the **new** branch (`exists: false`): the runtime branches from the policy's `parent` and
592
+ * brings the branch up with its declared TTL / compute settings / services. That's what makes
593
+ * a policy keyed on `!branch.exists` (the common "only configure new branches" shape) take
594
+ * effect on the very first `checkout` — a bare create + `apply` always saw `exists: true` and
595
+ * skipped that block.
596
+ *
597
+ * A branch that was created but whose policy failed to apply is reported through
598
+ * `policyFailure` rather than thrown: the branch is real, so `checkout` still needs to pin it
599
+ * (see the handler) instead of leaving it stranded behind an unchanged `.neon`.
600
+ */
601
+ const createBranchFromPolicyOnCheckout = async (props) => {
602
+ let config;
603
+ try {
604
+ ({config} = await loadConfigFromFile$1({ ...props.cwd ? { cwd: props.cwd } : {} }));
605
+ } catch (err) {
606
+ const message = err instanceof Error ? err.message : String(err);
607
+ if (/Could not find a Neon config file/i.test(message)) return null;
608
+ throw err;
609
+ }
610
+ await assertAiGatewayProvisionableFromCreds({
611
+ projectId: props.projectId,
612
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
613
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
614
+ ...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {},
615
+ config
616
+ });
617
+ try {
618
+ const { branchId, branchName, result } = await createBranch(config, {
619
+ projectId: props.projectId,
620
+ branchName: props.branchName,
621
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
622
+ ...props.apiHost ? { apiHost: props.apiHost } : {},
623
+ ...props.runtimeApi ? { api: props.runtimeApi } : {},
624
+ bundleFunction: neonctlBundler
625
+ });
626
+ log.info("Created branch %s (%s) from neon.ts policy.", branchName, branchId);
627
+ logPolicyResult(result, { color: props.color !== false });
628
+ return { branchId };
629
+ } catch (err) {
630
+ if (isPartialBranchCreateError(err)) {
631
+ log.info("Created branch %s (%s) from neon.ts policy.", err.branchName, err.branchId);
632
+ return {
633
+ branchId: err.branchId,
634
+ policyFailure: err.reason
635
+ };
636
+ }
637
+ throw err;
638
+ }
724
639
  };
640
+ //#endregion
641
+ export { applyCmd, applyFlags, applyPolicyOnCreate, builder, command, config_exports, createBranchFromPolicyOnCheckout, describe, envFlag, envPullFlag, handler, hasNeonConfigFile, initCmd, planCmd, status };