neon 3.0.0 → 3.1.1

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 (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -0,0 +1,531 @@
1
+ import { NEON_SERVICES } from "../neon_services.js";
2
+ import { ErrorCode, PlatformError, createNeonApiFromOptions, deriveCredentialScopes, resolveConfig } from "@neon/config/v1";
3
+ //#region ../../internals/env-core/dist/env.js
4
+ /**
5
+ * The Neon env core — resolving a branch's env from the Neon API, and projecting it into
6
+ * OS-level `{ KEY: value }` pairs.
7
+ *
8
+ * Private, and bundled into both consumers: `@neon/env` publishes it as `fetchEnv` /
9
+ * `toEntries`, and the `neon` CLI needs the credential-reuse half in `reuse-secrets.ts`.
10
+ * See `README.md` for why it is not published.
11
+ *
12
+ * The counterpart that reads `process.env` — `parseEnv` and its zod schemas — is not here. It
13
+ * has no consumer outside `@neon/env`, so it stays in that package and imports this.
14
+ */
15
+ /**
16
+ * Mapping between the {@link NeonEnv} property paths and the OS-level env-var keys used
17
+ * for cross-process transport (via `.env` files, `env run -- <cmd>`, or anything else
18
+ * that talks to `process.env`).
19
+ *
20
+ * Each top-level key here is a {@link NeonEnv} namespace; the inner record maps the
21
+ * camelCase property names exposed to TypeScript to the UPPER_SNAKE env-var names used
22
+ * by the OS. Keep this in sync with {@link postgresEnvSchema} / {@link authEnvSchema} /
23
+ * {@link dataApiEnvSchema}.
24
+ */
25
+ /**
26
+ * Neon's default branch owner role, created with every project. This is the role a
27
+ * `DATABASE_URL` should connect as.
28
+ */
29
+ const NEON_DEFAULT_OWNER_ROLE = "neondb_owner";
30
+ /**
31
+ * Neon's default database, created with every project. When a branch has several databases
32
+ * and none was requested, this is preferred for the `DATABASE_URL` so the common case (a
33
+ * user added a second database next to `neondb`) auto-picks without asking.
34
+ */
35
+ const NEON_DEFAULT_DATABASE = "neondb";
36
+ /**
37
+ * Roles Neon provisions for the Auth / Data API (PostgREST) stack. They exist to back
38
+ * RLS-scoped Data API requests authenticated by JWT — never to hold a `DATABASE_URL` —
39
+ * so they're skipped when auto-picking the connection role. Enabling Neon Auth or the
40
+ * Data API (`neon config apply`) adds these next to the owner role, which is why a plain
41
+ * branch routinely reports more than one role.
42
+ */
43
+ const NEON_MANAGED_AUTH_ROLES = /* @__PURE__ */ new Set([
44
+ "authenticator",
45
+ "anonymous",
46
+ "authenticated"
47
+ ]);
48
+ const NEON_ENV_VAR_KEYS = {
49
+ /**
50
+ * Branch identity. `NEON_BRANCH` carries the branch **name** and is injected into the
51
+ * Neon Functions runtime on every branch (including the default) by default. `env pull` /
52
+ * `neon dev` / `neon-env run` emit it too so local dev mirrors the deployed runtime.
53
+ */
54
+ branch: { name: "NEON_BRANCH" },
55
+ postgres: {
56
+ databaseUrl: "DATABASE_URL",
57
+ databaseUrlUnpooled: "DATABASE_URL_UNPOOLED"
58
+ },
59
+ auth: {
60
+ baseUrl: "NEON_AUTH_BASE_URL",
61
+ jwksUrl: "NEON_AUTH_JWKS_URL"
62
+ },
63
+ dataApi: { url: "NEON_DATA_API_URL" },
64
+ /**
65
+ * Object storage (Preview). The S3 SDKs read `AWS_*` from their standard config chain, so
66
+ * a branch credential + `neon dev` / `env pull` makes object storage work from env alone.
67
+ * `region` is injected under the SDK-standard `AWS_REGION`.
68
+ */
69
+ storage: {
70
+ accessKeyId: "AWS_ACCESS_KEY_ID",
71
+ secretAccessKey: "AWS_SECRET_ACCESS_KEY",
72
+ endpoint: "AWS_ENDPOINT_URL_S3",
73
+ region: "AWS_REGION"
74
+ },
75
+ /**
76
+ * AI Gateway (Preview). Exposed under the Neon-branded env vars the deployed Functions
77
+ * runtime injects: `apiKey` is the minted credential's bearer (`NEON_AI_GATEWAY_TOKEN`)
78
+ * and `baseUrl` is the bare branch gateway host (`NEON_AI_GATEWAY_BASE_URL`,
79
+ * `scheme://host`, no path). Clients like `@neon/ai-sdk-provider` read these and append the
80
+ * dialect route (`/v1`, `/openai/v1`, `/anthropic/v1`) themselves (https://github.com/vercel/ai/pull/15997).
81
+ */
82
+ aiGateway: {
83
+ apiKey: "NEON_AI_GATEWAY_TOKEN",
84
+ baseUrl: "NEON_AI_GATEWAY_BASE_URL"
85
+ }
86
+ };
87
+ /** Fail loudly when selected-key dependency planning and execution disagree. */
88
+ function requiredValue(value, description) {
89
+ if (value === null) throw new Error(`fetchEnv: missing ${description}.`);
90
+ return value;
91
+ }
92
+ /**
93
+ * The {@link fetchEnv} body, with the key selection as a plain argument and no generic
94
+ * narrowing. Exists for callers that compute the selection at runtime — notably
95
+ * {@link fetchEnvReusingSecrets}, which decides which keys it still needs by checking the
96
+ * branch — since the public overload's `keys` is bound to a literal union those callers cannot
97
+ * produce without asserting.
98
+ *
99
+ * `keys === null` selects everything the policy enables.
100
+ */
101
+ async function fetchEnvKeys(config, options, keys) {
102
+ const api = options.api ?? createApiFromOptions(options);
103
+ const projectId = options.projectId;
104
+ const { branch, desired } = await resolveBranchPolicy(config, options, api);
105
+ const selection = keys ? new Set(keys) : null;
106
+ const wants = (key) => selection === null || selection.has(key);
107
+ const result = {};
108
+ const K = NEON_ENV_VAR_KEYS;
109
+ const wantsPooled = wants(K.postgres.databaseUrl);
110
+ const wantsUnpooled = wants(K.postgres.databaseUrlUnpooled);
111
+ const wantsAuth = desired.authEnabled && (wants(K.auth.baseUrl) || wants(K.auth.jwksUrl));
112
+ const wantsDataApi = desired.dataApiEnabled && wants(K.dataApi.url);
113
+ const gatewayEnabled = desired.preview?.aiGatewayEnabled ?? false;
114
+ const needsUnpooled = wantsUnpooled || gatewayEnabled && wants(K.aiGateway.baseUrl);
115
+ const needsConnectionTarget = wantsPooled || needsUnpooled;
116
+ const needsDatabase = needsConnectionTarget || wantsDataApi;
117
+ const [roles, databases] = await Promise.all([needsConnectionTarget ? api.listBranchRoles(projectId, branch.id) : Promise.resolve([]), needsDatabase ? api.listBranchDatabases(projectId, branch.id) : Promise.resolve([])]);
118
+ const databaseName = needsDatabase ? pickDatabaseName(databases, branch, options.databaseName) : null;
119
+ const connectionTarget = needsConnectionTarget ? {
120
+ roleName: pickRoleName(roles, branch, options.roleName),
121
+ databaseName: requiredValue(databaseName, "database for a selected connection URI")
122
+ } : null;
123
+ const getConnectionUri = (pooled) => {
124
+ const target = requiredValue(connectionTarget, "role and database for a selected connection URI");
125
+ return api.getConnectionUri(projectId, {
126
+ branchId: branch.id,
127
+ ...target,
128
+ pooled
129
+ });
130
+ };
131
+ const [pooled, unpooled, authSnapshot, dataApiSnapshot] = await Promise.all([
132
+ wantsPooled ? getConnectionUri(true) : Promise.resolve(null),
133
+ needsUnpooled ? getConnectionUri(false) : Promise.resolve(null),
134
+ wantsAuth ? api.getNeonAuth(projectId, branch.id) : Promise.resolve(null),
135
+ wantsDataApi ? api.getNeonDataApi(projectId, branch.id, requiredValue(databaseName, "database for the selected Data API URL")) : Promise.resolve(null)
136
+ ]);
137
+ const postgres = {};
138
+ if (wantsPooled) postgres.databaseUrl = requiredValue(pooled, "pooled connection URI response").uri;
139
+ if (wantsUnpooled) postgres.databaseUrlUnpooled = requiredValue(unpooled, "direct connection URI response").uri;
140
+ if (Object.keys(postgres).length > 0) result.postgres = postgres;
141
+ if (wants(K.branch.name)) result.branch = { name: branch.name };
142
+ if (wantsAuth) {
143
+ if (!authSnapshot) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables auth but no Neon Auth integration is enabled on branch ${branch.name} (${branch.id}).`, "Enable it via `apply(config, { projectId, branchId })` (or `npx neon …`), in the Neon Console — then re-run fetchEnv. Or return auth.enabled=false."].join(" "), { details: {
144
+ projectId,
145
+ branchId: branch.id
146
+ } });
147
+ const auth = {};
148
+ if (wants(K.auth.baseUrl)) auth.baseUrl = authSnapshot.baseUrl ?? "";
149
+ if (wants(K.auth.jwksUrl)) auth.jwksUrl = authSnapshot.jwksUrl ?? "";
150
+ result.auth = auth;
151
+ }
152
+ if (wantsDataApi) {
153
+ if (!dataApiSnapshot) {
154
+ const selectedDatabase = requiredValue(databaseName, "database for the selected Data API URL");
155
+ throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy enables dataApi but no Data API integration is enabled on branch ${branch.name} (${branch.id}) database ${selectedDatabase}.`, "Enable it via `apply(config, { projectId, branchId })` or in the Neon Console — then re-run fetchEnv. Or return dataApi.enabled=false."].join(" "), { details: {
156
+ projectId,
157
+ branchId: branch.id,
158
+ databaseName: selectedDatabase
159
+ } });
160
+ }
161
+ result.dataApi = { url: dataApiSnapshot.url };
162
+ }
163
+ const storageEnabled = (desired.preview?.buckets.length ?? 0) > 0;
164
+ const wantsStorage = storageEnabled && (wants(K.storage.accessKeyId) || wants(K.storage.secretAccessKey) || wants(K.storage.endpoint) || wants(K.storage.region));
165
+ const wantsGateway = gatewayEnabled && (wants(K.aiGateway.apiKey) || wants(K.aiGateway.baseUrl));
166
+ const wantsStorageCredential = storageEnabled && (wants(K.storage.accessKeyId) || wants(K.storage.secretAccessKey));
167
+ const wantsGatewayCredential = gatewayEnabled && wants(K.aiGateway.apiKey);
168
+ const wantsCredential = wantsStorageCredential || wantsGatewayCredential;
169
+ if (wantsStorage || wantsGateway) {
170
+ let storage = null;
171
+ if (wantsStorage) {
172
+ storage = await api.getProjectBranchStorage(projectId, branch.id);
173
+ if (!storage) throw new PlatformError(ErrorCode.NotFound, [`fetchEnv: branch policy declares object storage (preview.buckets) but storage is not enabled on branch ${branch.name} (${branch.id}).`, "Enable it via `apply(config, { projectId, branchId })` (or in the Neon Console) — then re-run fetchEnv. Or remove preview.buckets."].join(" "), { details: {
174
+ projectId,
175
+ branchId: branch.id
176
+ } });
177
+ }
178
+ const secrets = wantsCredential ? await mintBranchCredential({
179
+ api,
180
+ projectId,
181
+ branchId: branch.id,
182
+ branchName: branch.name,
183
+ scopes: previewCredentialScopes(desired.preview, {
184
+ storage: wantsStorageCredential,
185
+ aiGateway: wantsGatewayCredential
186
+ })
187
+ }) : null;
188
+ if (storage) {
189
+ const storageEnv = {};
190
+ if (secrets && wants(K.storage.accessKeyId)) storageEnv.accessKeyId = secrets.accessKeyId;
191
+ if (secrets && wants(K.storage.secretAccessKey)) storageEnv.secretAccessKey = secrets.secretAccessKey;
192
+ if (wants(K.storage.endpoint)) storageEnv.endpoint = storage.s3Endpoint;
193
+ if (wants(K.storage.region)) storageEnv.region = storage.region;
194
+ result.storage = storageEnv;
195
+ }
196
+ if (wantsGateway) {
197
+ const gateway = {};
198
+ if (secrets && wants(K.aiGateway.apiKey)) gateway.apiKey = secrets.apiToken;
199
+ if (wants(K.aiGateway.baseUrl)) gateway.baseUrl = aiGatewayBaseUrl(branch.id, requiredValue(unpooled, "direct connection URI for the selected AI Gateway base URL").uri);
200
+ result.aiGateway = gateway;
201
+ }
202
+ }
203
+ return result;
204
+ }
205
+ /**
206
+ * Resolve the target branch and evaluate the policy against it — the first thing any
207
+ * branch-scoped operation needs. Shared by {@link fetchEnv} and {@link fetchEnvReusingSecrets}
208
+ * so the two agree on which branch they're talking about and what it has enabled.
209
+ */
210
+ async function resolveBranchPolicy(config, options, api) {
211
+ const projectId = options.projectId;
212
+ const branches = await api.listBranches(projectId);
213
+ if (branches.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: project ${projectId} has no branches.`, "Deploy your neon.ts policy (or create a branch) first, or pick a different project id."].join(" "), { details: { projectId } });
214
+ const branchRef = options.branch ?? options.branchId;
215
+ if (!branchRef) throw new PlatformError(ErrorCode.BranchNotFound, ["fetchEnv: no branch provided.", "Pass `branch` with a branch name (e.g. `main`) or id (`br-…`)."].join(" "), { details: { projectId } });
216
+ const branch = resolveBranch(branchRef, branches);
217
+ return {
218
+ branch,
219
+ desired: resolveConfig(config, {
220
+ name: branch.name,
221
+ id: branch.id,
222
+ exists: true,
223
+ ...branch.parentId ? { parentId: branch.parentId } : {},
224
+ isDefault: branch.isDefault,
225
+ isProtected: branch.protected,
226
+ ...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
227
+ })
228
+ };
229
+ }
230
+ /**
231
+ * Scopes the branch credential should carry for a resolved branch policy and optional key
232
+ * selection. Only object storage and the AI Gateway *require* a credential; functions never
233
+ * force one, but `functions:invoke` rides along when another selected feature mints one.
234
+ */
235
+ function previewCredentialScopes(preview, selected) {
236
+ if (!preview) return [];
237
+ const storage = preview.buckets.length > 0 && (selected?.storage ?? true);
238
+ const aiGateway = preview.aiGatewayEnabled && (selected?.aiGateway ?? true);
239
+ if (!storage && !aiGateway) return [];
240
+ return deriveCredentialScopes({
241
+ storage,
242
+ aiGateway,
243
+ functions: preview.functions.length > 0
244
+ });
245
+ }
246
+ /** The `name` this tool stamps on every credential it mints, so it can recognize its own. */
247
+ function credentialName(branchName) {
248
+ return `neon-env ${branchName}`;
249
+ }
250
+ /** The env-var keys a branch credential's secrets surface under, in emit order. */
251
+ function credentialEnvKeys(flags) {
252
+ return [...flags.storage ? [NEON_ENV_VAR_KEYS.storage.accessKeyId, NEON_ENV_VAR_KEYS.storage.secretAccessKey] : [], ...flags.aiGateway ? [NEON_ENV_VAR_KEYS.aiGateway.apiKey] : []];
253
+ }
254
+ /**
255
+ * Every OS-level env var a resolved branch policy produces, in emit order. Lets a caller
256
+ * subtract the ones it already holds and pass the rest as {@link fetchEnv}'s `keys`, without
257
+ * re-deriving which vars a policy implies.
258
+ */
259
+ function policyEnvKeys(desired) {
260
+ const K = NEON_ENV_VAR_KEYS;
261
+ return [
262
+ K.postgres.databaseUrl,
263
+ K.postgres.databaseUrlUnpooled,
264
+ K.branch.name,
265
+ ...desired.authEnabled ? [K.auth.baseUrl, K.auth.jwksUrl] : [],
266
+ ...desired.dataApiEnabled ? [K.dataApi.url] : [],
267
+ ...(desired.preview?.buckets.length ?? 0) > 0 ? [
268
+ K.storage.accessKeyId,
269
+ K.storage.secretAccessKey,
270
+ K.storage.endpoint,
271
+ K.storage.region
272
+ ] : [],
273
+ ...desired.preview?.aiGatewayEnabled ? [K.aiGateway.apiKey, K.aiGateway.baseUrl] : []
274
+ ];
275
+ }
276
+ /**
277
+ * Mint the branch credential backing object storage / the AI Gateway.
278
+ *
279
+ * `api_token` and `s3_secret_access_key` come back **exactly once** — they are not stored
280
+ * server-side and the list endpoint returns metadata only — so the caller's copy is the only
281
+ * copy. That is why {@link fetchEnv} mints rather than fetches: there is nothing to fetch. A
282
+ * caller that already holds a valid copy should leave the secret keys out of `keys` (see
283
+ * {@link fetchEnvReusingSecrets}) instead of minting one it will discard.
284
+ */
285
+ async function mintBranchCredential(args) {
286
+ const minted = await args.api.createCredential(args.projectId, args.branchId, {
287
+ scopes: args.scopes,
288
+ principalType: "user",
289
+ name: credentialName(args.branchName)
290
+ });
291
+ return {
292
+ accessKeyId: minted.tokenId,
293
+ secretAccessKey: minted.s3SecretAccessKey,
294
+ apiToken: minted.apiToken
295
+ };
296
+ }
297
+ /**
298
+ * The AI Gateway is a **branch-scoped host** — `<branchId>-api.ai.<host-suffix>` — NOT the
299
+ * control-plane API origin. Derive the suffix from the branch's own Postgres connection host
300
+ * by dropping only the endpoint label (the first segment) and keeping everything after it,
301
+ * including any infra cell prefix (`c-N.`): a connection host of
302
+ * `ep-x.c-3.us-east-2.aws.neon.tech` yields the gateway host
303
+ * `<branchId>-api.ai.c-3.us-east-2.aws.neon.tech`. The cell prefix is **load-bearing** —
304
+ * the gateway is cell-routed, so dropping `c-N.` resolves to the wrong (or no) host.
305
+ */
306
+ function aiGatewayHost(branchId, connectionUri) {
307
+ let connectionHost = "";
308
+ try {
309
+ connectionHost = new URL(connectionUri).hostname;
310
+ } catch {
311
+ connectionHost = "";
312
+ }
313
+ return `${branchId}-api.ai.${connectionHost.split(".").slice(1).join(".")}`;
314
+ }
315
+ /** The AI Gateway's bare base URL (`NEON_AI_GATEWAY_BASE_URL`) on the branch gateway host. */
316
+ function aiGatewayBaseUrl(branchId, connectionUri) {
317
+ return `https://${aiGatewayHost(branchId, connectionUri)}`;
318
+ }
319
+ function createApiFromOptions(options) {
320
+ return createNeonApiFromOptions("fetchEnv", {
321
+ ...options.apiKey ? { apiKey: options.apiKey } : {},
322
+ ...options.apiHost ? { apiHost: options.apiHost } : {}
323
+ });
324
+ }
325
+ /**
326
+ * Resolve a branch ref — a name or an id — to a concrete branch. Matches by id first
327
+ * (exact `br-…`), then by name; both are unique within a project, so the lookup is
328
+ * unambiguous. This lets `.neon` files written by `neonctl` (which pin the branch *name*)
329
+ * and explicit `br-…` ids both work.
330
+ */
331
+ function resolveBranch(branch, branches) {
332
+ const match = branches.find((b) => b.id === branch) ?? branches.find((b) => b.name === branch);
333
+ if (match) return match;
334
+ throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${JSON.stringify(branch)} not found on project (matched by id or name).`, `Existing branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ")}.`].join(" "), { details: {
335
+ branch,
336
+ available: branches.map((b) => `${b.name} (${b.id})`)
337
+ } });
338
+ }
339
+ function pickRoleName(roles, branch, requested) {
340
+ if (requested) {
341
+ if (!roles.some((r) => r.name === requested)) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: role "${requested}" not found on branch ${branch.name} (${branch.id}).`, `Existing roles: ${roles.map((r) => r.name).join(", ") || "(none)"}.`].join(" "), { details: {
342
+ branchId: branch.id,
343
+ roleName: requested,
344
+ availableRoles: roles.map((r) => r.name)
345
+ } });
346
+ return requested;
347
+ }
348
+ if (roles.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${branch.name} (${branch.id}) has no roles.`, "Create one via the Neon console or pass `roleName` explicitly."].join(" "), { details: { branchId: branch.id } });
349
+ if (roles.length === 1) return roles[0].name;
350
+ const owner = roles.find((r) => r.name === NEON_DEFAULT_OWNER_ROLE);
351
+ if (owner) return owner.name;
352
+ const appRoles = roles.filter((r) => !NEON_MANAGED_AUTH_ROLES.has(r.name));
353
+ if (appRoles.length === 1) return appRoles[0].name;
354
+ throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${roles.length} roles and none is "${NEON_DEFAULT_OWNER_ROLE}"; cannot auto-pick.`, `Pass \`roleName\` explicitly. Available: ${roles.map((r) => r.name).join(", ")}.`].join(" "), { details: {
355
+ branchId: branch.id,
356
+ availableRoles: roles.map((r) => r.name)
357
+ } });
358
+ }
359
+ function pickDatabaseName(databases, branch, requested) {
360
+ if (requested) {
361
+ if (!databases.some((d) => d.name === requested)) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: database "${requested}" not found on branch ${branch.name} (${branch.id}).`, `Existing databases: ${databases.map((d) => d.name).join(", ") || "(none)"}.`].join(" "), { details: {
362
+ branchId: branch.id,
363
+ databaseName: requested,
364
+ availableDatabases: databases.map((d) => d.name)
365
+ } });
366
+ return requested;
367
+ }
368
+ if (databases.length === 0) throw new PlatformError(ErrorCode.BranchNotFound, [`fetchEnv: branch ${branch.name} (${branch.id}) has no databases.`, "Create one via the Neon console or pass `databaseName` explicitly."].join(" "), { details: { branchId: branch.id } });
369
+ const neondb = databases.find((d) => d.name === NEON_DEFAULT_DATABASE);
370
+ if (neondb) return neondb.name;
371
+ if (databases.length === 1) return databases[0].name;
372
+ throw new PlatformError(ErrorCode.AmbiguousBranchAuth, [`fetchEnv: branch ${branch.name} (${branch.id}) has ${databases.length} databases and none is named "${NEON_DEFAULT_DATABASE}"; cannot auto-pick.`, `Rename one to "${NEON_DEFAULT_DATABASE}" or keep a single database on the branch (or, when calling fetchEnv directly, pass \`databaseName\`). Available: ${databases.map((d) => d.name).join(", ")}.`].join(" "), { details: {
373
+ branchId: branch.id,
374
+ availableDatabases: databases.map((d) => d.name)
375
+ } });
376
+ }
377
+ /**
378
+ * Project a fully-resolved {@link NeonEnv} into the OS-level `{ KEY: value }` pairs used
379
+ * for cross-process transport. Named after the web-platform `.entries()` convention
380
+ * (`URLSearchParams` / `Headers` / `FormData`); returns a `Record` rather than an
381
+ * iterator of tuples since that's the shape env injection needs (wrap with
382
+ * `Object.entries(...)` if you want literal `[key, value]` pairs). Used by `neon-env run`
383
+ * to inject the vars into a subprocess's `process.env`.
384
+ *
385
+ * Walks the value at runtime so it works for any `NeonEnv<C>` regardless of which
386
+ * conditional namespaces are present.
387
+ */
388
+ function toEntries(env) {
389
+ const out = {};
390
+ const put = (key, value) => {
391
+ if (value !== void 0) out[key] = value;
392
+ };
393
+ const K = NEON_ENV_VAR_KEYS;
394
+ put(K.postgres.databaseUrl, env.postgres?.databaseUrl);
395
+ put(K.postgres.databaseUrlUnpooled, env.postgres?.databaseUrlUnpooled);
396
+ put(K.branch.name, env.branch?.name);
397
+ put(K.auth.baseUrl, env.auth?.baseUrl);
398
+ put(K.auth.jwksUrl, env.auth?.jwksUrl);
399
+ put(K.dataApi.url, env.dataApi?.url);
400
+ put(K.storage.accessKeyId, env.storage?.accessKeyId);
401
+ put(K.storage.secretAccessKey, env.storage?.secretAccessKey);
402
+ put(K.storage.endpoint, env.storage?.endpoint);
403
+ put(K.storage.region, env.storage?.region);
404
+ put(K.aiGateway.apiKey, env.aiGateway?.apiKey);
405
+ put(K.aiGateway.baseUrl, env.aiGateway?.baseUrl);
406
+ return out;
407
+ }
408
+ //#endregion
409
+ //#region src/env_services.ts
410
+ /**
411
+ * The services `env pull --service` can select: every Neon service that produces branch env
412
+ * vars. `functions` is the one left out — a function's env comes from the local `neon.ts`,
413
+ * never from the branch, so there is nothing to pull.
414
+ */
415
+ const ENV_PULL_SERVICES = NEON_SERVICES.filter((service) => service !== "functions");
416
+ /** Why the services `env pull` leaves out are not selectable, for the refusal message. */
417
+ const ENV_PULL_UNAVAILABLE = { functions: "a function's env comes from your neon.ts, not from the branch, so there is nothing to pull" };
418
+ /** Every OS-level env var `env pull` can write, in stable emit order. */
419
+ const ENV_PULL_KEYS = [
420
+ ...Object.values(NEON_ENV_VAR_KEYS.postgres),
421
+ NEON_ENV_VAR_KEYS.branch.name,
422
+ ...Object.values(NEON_ENV_VAR_KEYS.auth),
423
+ ...Object.values(NEON_ENV_VAR_KEYS.dataApi),
424
+ ...Object.values(NEON_ENV_VAR_KEYS.storage),
425
+ ...Object.values(NEON_ENV_VAR_KEYS.aiGateway)
426
+ ];
427
+ /** The OS-level env vars each service contributes to a pulled `.env`. */
428
+ const SERVICE_ENV_KEYS = {
429
+ postgres: Object.values(NEON_ENV_VAR_KEYS.postgres),
430
+ auth: Object.values(NEON_ENV_VAR_KEYS.auth),
431
+ "data-api": Object.values(NEON_ENV_VAR_KEYS.dataApi),
432
+ "object-storage": Object.values(NEON_ENV_VAR_KEYS.storage),
433
+ "ai-gateway": Object.values(NEON_ENV_VAR_KEYS.aiGateway),
434
+ functions: []
435
+ };
436
+ /**
437
+ * The subset of {@link SERVICE_ENV_KEYS} a pull *owns*, and so may prune from the target file
438
+ * when the branch no longer has it. Object storage is deliberately absent: it is emitted under
439
+ * the third-party `AWS_*` names, which collide with credentials a user may set by hand, so
440
+ * `env pull` only ever writes them.
441
+ */
442
+ const SERVICE_OWNED_ENV_KEYS = {
443
+ ...SERVICE_ENV_KEYS,
444
+ "object-storage": []
445
+ };
446
+ /**
447
+ * Branch identity. Not a service — every branch has a name — so a scoped pull refreshes it
448
+ * alongside whatever services were selected.
449
+ */
450
+ const BRANCH_ENV_KEY = NEON_ENV_VAR_KEYS.branch.name;
451
+ /** The service that produces a key, or `null` for branch identity. */
452
+ const ENV_KEY_SERVICE = {
453
+ DATABASE_URL: "postgres",
454
+ DATABASE_URL_UNPOOLED: "postgres",
455
+ NEON_BRANCH: null,
456
+ NEON_AUTH_BASE_URL: "auth",
457
+ NEON_AUTH_JWKS_URL: "auth",
458
+ NEON_DATA_API_URL: "data-api",
459
+ AWS_ACCESS_KEY_ID: "object-storage",
460
+ AWS_SECRET_ACCESS_KEY: "object-storage",
461
+ AWS_ENDPOINT_URL_S3: "object-storage",
462
+ AWS_REGION: "object-storage",
463
+ NEON_AI_GATEWAY_TOKEN: "ai-gateway",
464
+ NEON_AI_GATEWAY_BASE_URL: "ai-gateway"
465
+ };
466
+ const serviceForEnvKey = (key) => ENV_KEY_SERVICE[key];
467
+ /** Services that must be resolved to produce the selected env keys. */
468
+ const servicesForEnvKeys = (keys) => ENV_PULL_SERVICES.filter((service) => keys.some((key) => ENV_KEY_SERVICE[key] === service));
469
+ /** Every env var the selected services contribute, plus branch identity. */
470
+ const envServiceKeys = (services) => {
471
+ const keys = /* @__PURE__ */ new Set([BRANCH_ENV_KEY]);
472
+ for (const service of services) for (const key of SERVICE_ENV_KEYS[service]) keys.add(key);
473
+ return keys;
474
+ };
475
+ /**
476
+ * The env vars a pull scoped to `services` may prune. Narrower than the unscoped set on
477
+ * purpose: `env pull -s ai-gateway` says nothing about `DATABASE_URL`, so it must leave it
478
+ * alone rather than treat its absence from this pull as "the branch no longer has it".
479
+ */
480
+ const ownedEnvServiceKeys = (services) => services.flatMap((service) => SERVICE_OWNED_ENV_KEYS[service]);
481
+ /**
482
+ * The exact env vars an explicit selection writes. Services contribute their complete
483
+ * bundles plus branch identity; `--env` contributes only the named keys. The two selectors
484
+ * compose as a union.
485
+ */
486
+ const envKeysForSelection = (services, envKeys) => {
487
+ const selected = services.length > 0 ? envServiceKeys(services) : /* @__PURE__ */ new Set();
488
+ for (const key of envKeys) selected.add(key);
489
+ if (selected.has(NEON_ENV_VAR_KEYS.storage.accessKeyId) !== selected.has(NEON_ENV_VAR_KEYS.storage.secretAccessKey)) throw new Error("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be selected together: they are two halves of one newly issued object-storage credential. Add the missing key to --env, or use --service object-storage.");
490
+ return ENV_PULL_KEYS.filter((key) => selected.has(key));
491
+ };
492
+ /** Parse repeated or comma-separated `--env` values into canonical env-key order. */
493
+ const parseEnvPullKeys = (raw, flag) => {
494
+ const names = raw.flatMap((value) => value.split(",")).map((name) => name.trim()).filter((name) => name !== "");
495
+ const supported = `Supported values: ${ENV_PULL_KEYS.join(", ")}.`;
496
+ if (names.length === 0) throw new Error(`${flag} needs at least one env variable. ${supported}`);
497
+ const unknown = names.filter((name) => !ENV_PULL_KEYS.some((key) => key === name));
498
+ if (unknown.length > 0) {
499
+ const displayNames = unknown.map(redactUnknownEnvValue);
500
+ const suggestions = [...new Set(unknown.map(suggestEnvPullKey).filter((key) => key !== null))];
501
+ const suggestion = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
502
+ throw new Error(`Unknown env variable${unknown.length === 1 ? "" : "s"} ${displayNames.join(", ")}.${suggestion} ${supported}`);
503
+ }
504
+ return ENV_PULL_KEYS.filter((key) => names.includes(key));
505
+ };
506
+ const redactUnknownEnvValue = (value) => {
507
+ const separator = value.indexOf("=");
508
+ if (separator !== -1) {
509
+ const key = value.slice(0, separator);
510
+ return ENV_PULL_KEYS.some((supportedKey) => supportedKey === key) ? `${key}=<redacted>` : "<redacted invalid value>";
511
+ }
512
+ return "<redacted invalid value>";
513
+ };
514
+ const suggestEnvPullKey = (value) => {
515
+ if (value.includes("=")) return null;
516
+ const closest = ENV_PULL_KEYS.map((key) => [key, editDistance(value, key)]).sort((a, b) => a[1] - b[1])[0];
517
+ return closest && closest[1] <= 2 ? closest[0] : null;
518
+ };
519
+ const editDistance = (left, right) => {
520
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
521
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
522
+ const current = [leftIndex];
523
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) current[rightIndex] = Math.min((previous[rightIndex] ?? 0) + 1, (current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1));
524
+ previous = current;
525
+ }
526
+ return previous[right.length] ?? right.length;
527
+ };
528
+ /** Narrow yargs' array option value without accepting any other runtime shape. */
529
+ const envPullFlagValue = (value) => Array.isArray(value) ? value.map(String) : void 0;
530
+ //#endregion
531
+ export { policyEnvKeys as _, envKeysForSelection as a, toEntries as b, ownedEnvServiceKeys as c, servicesForEnvKeys as d, NEON_ENV_VAR_KEYS as f, fetchEnvKeys as g, credentialName as h, ENV_PULL_UNAVAILABLE as i, parseEnvPullKeys as l, credentialEnvKeys as m, ENV_PULL_KEYS as n, envPullFlagValue as o, createApiFromOptions as p, ENV_PULL_SERVICES as r, envServiceKeys as s, BRANCH_ENV_KEY as t, serviceForEnvKey as u, previewCredentialScopes as v, resolveBranchPolicy as y };
@@ -0,0 +1,151 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ //#region ../../internals/cli-core/dist/paths.js
4
+ /**
5
+ * # Where the Neon CLIs keep their files on disk
6
+ *
7
+ **Deliberately impure.** It reads environment variables and touches the filesystem, which
8
+ * `@neon/config` — the package this used to be a subpath of — must never do from its root
9
+ * export. It lives here instead of there precisely so that a policy-facing package does not
10
+ * carry implementor-only code.
11
+ *
12
+ * It exists because three separate readers each grew their own answer to "where is the
13
+ * config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
14
+ * not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
15
+ * flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
16
+ * credentials somewhere the other two never looked.
17
+ *
18
+ * ## The directory
19
+ *
20
+ * `neon` is the current name; `neonctl` is the legacy one, kept readable forever. Resolution,
21
+ * each entry winning over the next:
22
+ *
23
+ * 1. An explicit directory (a `--config-dir` flag) — **exact**, no legacy fallback.
24
+ * 2. `NEON_CONFIG_DIR` — exact.
25
+ * 3. `NEONCTL_CONFIG_DIR` (legacy name) — exact.
26
+ * 4. `$XDG_CONFIG_HOME/neon`, else `<home>/.config/neon`.
27
+ *
28
+ * An explicitly chosen directory is never paired with a fallback: `--config-dir /tmp/ci` that
29
+ * quietly read `~/.config/neonctl` would defeat the point of passing it.
30
+ *
31
+ * ## The files
32
+ *
33
+ * {@link resolveConfigFile} answers "which path should I use for this file", and it is the
34
+ * same answer for reading and writing:
35
+ *
36
+ * - Present in `neon/` → use it.
37
+ * - Present only in `neonctl/` → **use it there, in place.** An existing credentials file is
38
+ * never copied or moved, so nothing is left behind to go stale and no other tool starts
39
+ * reading an abandoned token.
40
+ * - Present in neither → the new location. New files only ever appear under `neon/`.
41
+ */
42
+ /** Current directory name. New files are created here. */
43
+ const CONFIG_DIR_NAME = "neon";
44
+ /** Legacy directory name, read forever so existing installs keep working untouched. */
45
+ const LEGACY_CONFIG_DIR_NAME = "neonctl";
46
+ /** Where files are created. See the module docs for the precedence. */
47
+ function configDir(options = {}) {
48
+ const explicit = explicitDir(options);
49
+ if (explicit) return explicit;
50
+ return join(configHome(options.env ?? process.env), CONFIG_DIR_NAME);
51
+ }
52
+ /**
53
+ * The legacy directory, or `undefined` when the location was chosen explicitly (in which
54
+ * case there is no legacy counterpart to fall back to).
55
+ */
56
+ function legacyConfigDir(options = {}) {
57
+ if (explicitDir(options)) return void 0;
58
+ return join(configHome(options.env ?? process.env), LEGACY_CONFIG_DIR_NAME);
59
+ }
60
+ /**
61
+ * Resolve one file inside the config directory. Prefers the current location, falls back to
62
+ * an existing legacy file **in place**, and otherwise points at the current location so new
63
+ * files are created there.
64
+ */
65
+ function resolveConfigFile(fileName, options = {}) {
66
+ const dir = configDir(options);
67
+ const current = resolve(dir, fileName);
68
+ if (existsSync(current)) return {
69
+ path: current,
70
+ dir,
71
+ isLegacy: false,
72
+ exists: true
73
+ };
74
+ const legacyDir = legacyConfigDir(options);
75
+ if (legacyDir) {
76
+ const legacy = resolve(legacyDir, fileName);
77
+ if (existsSync(legacy)) return {
78
+ path: legacy,
79
+ dir: legacyDir,
80
+ isLegacy: true,
81
+ exists: true
82
+ };
83
+ }
84
+ return {
85
+ path: current,
86
+ dir,
87
+ isLegacy: false,
88
+ exists: false
89
+ };
90
+ }
91
+ /** `$XDG_CONFIG_HOME`, else `<home>/.config`. Falls back to a relative `.config` with no home. */
92
+ function configHome(env) {
93
+ const xdg = nonEmpty(env.XDG_CONFIG_HOME);
94
+ if (xdg) return xdg;
95
+ const home = nonEmpty(env.HOME) ?? nonEmpty(env.USERPROFILE);
96
+ return home ? join(home, ".config") : ".config";
97
+ }
98
+ function explicitDir(options) {
99
+ const env = options.env ?? process.env;
100
+ return nonEmpty(options.dir) ?? nonEmpty(env.NEON_CONFIG_DIR) ?? nonEmpty(env.NEONCTL_CONFIG_DIR);
101
+ }
102
+ function nonEmpty(value) {
103
+ if (typeof value !== "string") return void 0;
104
+ const trimmed = value.trim();
105
+ return trimmed === "" ? void 0 : trimmed;
106
+ }
107
+ const CREDENTIALS_FILE = "credentials.json";
108
+ /**
109
+ * Default for `--config-dir`: `$XDG_CONFIG_HOME/neon`, else `~/.config/neon`.
110
+ *
111
+ * The directory was called `neonctl` until the CLI was renamed. An existing one is still read —
112
+ * see {@link credentialsPath} — but it is never written to, moved, or deleted.
113
+ */
114
+ const defaultDir = configDir();
115
+ /**
116
+ * Where this invocation's `credentials.json` lives.
117
+ *
118
+ * When `--config-dir` was left at its default, an existing file in the legacy `neonctl`
119
+ * directory is used **in place**: an install that predates the rename keeps working, and its
120
+ * credentials are never duplicated into a second location where one copy could go stale while
121
+ * another tool still reads it.
122
+ *
123
+ * A `--config-dir` the user actually passed is used exactly as given. Falling back out of an
124
+ * explicitly chosen directory would defeat the reason for choosing it — a CI run pointed at a
125
+ * scratch directory must never pick up a developer's real credentials.
126
+ */
127
+ const credentialsPath = (dir) => resolveConfigFile(CREDENTIALS_FILE, dir === defaultDir ? {} : { dir }).path;
128
+ /**
129
+ * Whether a credentials file is one the CLI created, rather than a path a profile adopted.
130
+ *
131
+ * Anything that deletes a credential has to ask this first. A profile entry may point anywhere —
132
+ * that is what makes adopting an existing directory a one-line edit — and a file we did not
133
+ * create is not ours to remove.
134
+ */
135
+ const isInsideConfigDir = (configDirectory, file) => `${resolve(file)}/`.startsWith(`${resolve(configDirectory)}/`);
136
+ /**
137
+ * Whether a credentials file is one the CLI owns, counting the legacy `neonctl` directory.
138
+ *
139
+ * {@link credentialsPath} deliberately reads an existing legacy file in place rather than
140
+ * migrating it, so for a default config directory that file is ours even though it sits outside
141
+ * `neon/`. Judging ownership on the current directory alone would call an install that predates
142
+ * the rename "adopted".
143
+ */
144
+ const isOwnedCredentialPath = (configDirectory, file) => {
145
+ if (isInsideConfigDir(configDirectory, file)) return true;
146
+ if (configDirectory !== defaultDir) return false;
147
+ const legacy = legacyConfigDir();
148
+ return legacy !== void 0 && isInsideConfigDir(legacy, file);
149
+ };
150
+ //#endregion
151
+ export { isOwnedCredentialPath as a, isInsideConfigDir as i, credentialsPath as n, resolveConfigFile as o, defaultDir as r, CREDENTIALS_FILE as t };