neon 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +354 -517
- package/dist/_shared/env-core/reuse-secrets.js +159 -203
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +7 -5
package/dist/dev/env.js
CHANGED
|
@@ -1,446 +1,365 @@
|
|
|
1
|
-
import { createNeonApiFromOptions, loadConfigFromFile, } from "@neon/config";
|
|
2
|
-
import { plan, pullConfig } from "@neon/config-runtime";
|
|
3
|
-
import { NEON_ENV_VAR_KEYS } from "../_shared/env-core/env.js";
|
|
4
|
-
import { fetchEnvReusingSecrets, } from "../_shared/env-core/reuse-secrets.js";
|
|
5
1
|
import { log } from "../log.js";
|
|
6
2
|
import { getCliName } from "../utils/cli_name.js";
|
|
3
|
+
import { NEON_ENV_VAR_KEYS } from "../_shared/env-core/env.js";
|
|
4
|
+
import { formatInstallCommand, resolvePackageManager } from "../utils/package_manager.js";
|
|
5
|
+
import { fetchEnvReusingSecrets } from "../_shared/env-core/reuse-secrets.js";
|
|
6
|
+
import { createNeonApiFromOptions, loadConfigFromFile } from "@neon/config";
|
|
7
|
+
import { plan, pullConfig } from "@neon/config-runtime";
|
|
8
|
+
//#region src/dev/env.ts
|
|
7
9
|
/** The API-targeting options every runtime call forwards from the context. */
|
|
8
10
|
const apiOptions = (ctx) => ({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
...ctx.apiKey ? { apiKey: ctx.apiKey } : {},
|
|
12
|
+
...ctx.apiHost ? { apiHost: ctx.apiHost } : {},
|
|
13
|
+
...ctx.api ? { api: ctx.api } : {}
|
|
12
14
|
});
|
|
13
15
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
16
|
+
* Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
|
|
17
|
+
* Data API, a bucket, the AI Gateway) that the linked remote branch does not
|
|
18
|
+
* have yet. Unlike every other failure in {@link resolveDevEnv} — which degrades
|
|
19
|
+
* to "run without injection" — this is a hard stop: the user's intent (a policy)
|
|
20
|
+
* cannot be honored, and silently dropping the secret would be more confusing
|
|
21
|
+
* than refusing to start. The fix is to provision the resource first.
|
|
22
|
+
*/
|
|
23
|
+
var DevEnvMismatchError = class extends Error {
|
|
24
|
+
constructor(..._args) {
|
|
25
|
+
super(..._args);
|
|
26
|
+
this.name = "DevEnvMismatchError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
27
29
|
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
30
|
+
* Signals that no project/branch context could be resolved, so there is nothing to
|
|
31
|
+
* resolve env from. `resolveDevEnv` degrades on this (dev runs without injection);
|
|
32
|
+
* `env pull` surfaces it (an explicit pull needs a branch).
|
|
33
|
+
*/
|
|
34
|
+
var MissingBranchContextError = class extends Error {
|
|
35
|
+
constructor(..._args2) {
|
|
36
|
+
super(..._args2);
|
|
37
|
+
this.name = "MissingBranchContextError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
38
40
|
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
41
|
+
* Thrown when an explicit `--service` selection names a service the branch does not have.
|
|
42
|
+
* Unlike the policy path — where the same situation is a {@link DevEnvMismatchError} pointing
|
|
43
|
+
* at `deploy` — the user named the service on the command line, so the fix is to provision it
|
|
44
|
+
* or drop it from the selection.
|
|
45
|
+
*/
|
|
46
|
+
var ServiceNotOnBranchError = class extends Error {
|
|
47
|
+
constructor(..._args3) {
|
|
48
|
+
super(..._args3);
|
|
49
|
+
this.name = "ServiceNotOnBranchError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
50
52
|
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
if (ctx.projectId && ctx.branchId) {
|
|
98
|
-
const pulled = await pullConfig({
|
|
99
|
-
projectId: ctx.projectId,
|
|
100
|
-
branchId: ctx.branchId,
|
|
101
|
-
...apiOptions(ctx),
|
|
102
|
-
});
|
|
103
|
-
// `pulled.config` is already a `Config` (static auth/dataApi toggles, any
|
|
104
|
-
// object-storage `preview.buckets`, and a branch tuning closure), so it feeds
|
|
105
|
-
// straight into fetchEnv — no wrapping needed. pullConfig excludes functions and
|
|
106
|
-
// the AI Gateway (neither can be faithfully read back), so fetchEnv never probes
|
|
107
|
-
// the functions API here and only mints a storage credential when a bucket exists.
|
|
108
|
-
if (!ctx.implyAiGateway) {
|
|
109
|
-
return await fetchAndProject(pulled.config, ctx);
|
|
110
|
-
}
|
|
111
|
-
return await resolveWithImpliedGateway(pulled.config, ctx, {
|
|
112
|
-
projectId: ctx.projectId,
|
|
113
|
-
branchId: ctx.branchId,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / ` +
|
|
117
|
-
`\`${getCliName()} checkout\`) or pass --project-id and --branch.`);
|
|
53
|
+
* Resolve the branch's Neon env vars (pooled / direct `DATABASE_URL`, plus Auth /
|
|
54
|
+
* Data API when enabled) into a `{ KEY: value }` map. Shared by `neon dev` (which
|
|
55
|
+
* injects them) and `neon env pull` (which writes them to a `.env` file).
|
|
56
|
+
*
|
|
57
|
+
* Tiered:
|
|
58
|
+
*
|
|
59
|
+
* 0. {@link DevEnvContext.services} is set -> that selection *is* the policy, and any
|
|
60
|
+
* `neon.ts` is ignored. See {@link resolveSelectedServices}.
|
|
61
|
+
* 1. a `neon.ts` policy is found -> the policy is the source of truth. We first
|
|
62
|
+
* check it against the branch's live state (`plan`); if it declares a resource
|
|
63
|
+
* the branch is missing, we stop with a {@link DevEnvMismatchError} pointing at
|
|
64
|
+
* `neonctl deploy`. Otherwise `fetchEnv` evaluates the policy.
|
|
65
|
+
* 2. no `neon.ts`, but a project + branch are known -> `pullConfig` reads the
|
|
66
|
+
* branch's live state (Auth / Data API enablement plus any object-storage
|
|
67
|
+
* buckets) into a config, then `fetchEnv` resolves what is actually enabled —
|
|
68
|
+
* so a branch with a bucket gets its `AWS_*` storage vars pulled with no policy.
|
|
69
|
+
* With {@link DevEnvContext.implyAiGateway}, the AI Gateway is added on top, since
|
|
70
|
+
* `pullConfig` cannot read it back.
|
|
71
|
+
* 3. otherwise -> throw {@link MissingBranchContextError}.
|
|
72
|
+
*
|
|
73
|
+
* Unlike {@link resolveDevEnv}, this never swallows errors — callers decide how to
|
|
74
|
+
* handle them.
|
|
75
|
+
*/
|
|
76
|
+
const resolveNeonEnvVars = async (ctx) => {
|
|
77
|
+
if (ctx.services) return await resolveSelectedServices(ctx, ctx.services);
|
|
78
|
+
const config = await loadNeonConfig(ctx.cwd);
|
|
79
|
+
if (config) {
|
|
80
|
+
if (!ctx.projectId || !ctx.branchId) throw new MissingBranchContextError(`Found a neon.ts but could not resolve the project/branch. Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass --project-id / --branch.`);
|
|
81
|
+
const envConfig = withoutPreviewFunctions(config);
|
|
82
|
+
await assertPolicyMatchesBranch(envConfig, ctx);
|
|
83
|
+
return await fetchAndProject(envConfig, ctx);
|
|
84
|
+
}
|
|
85
|
+
if (ctx.projectId && ctx.branchId) {
|
|
86
|
+
const pulled = await pullConfig({
|
|
87
|
+
projectId: ctx.projectId,
|
|
88
|
+
branchId: ctx.branchId,
|
|
89
|
+
...apiOptions(ctx)
|
|
90
|
+
});
|
|
91
|
+
if (!ctx.implyAiGateway) return await fetchAndProject(pulled.config, ctx);
|
|
92
|
+
return await resolveWithImpliedGateway(pulled.config, ctx, {
|
|
93
|
+
projectId: ctx.projectId,
|
|
94
|
+
branchId: ctx.branchId
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / \`${getCliName()} checkout\`) or pass --project-id and --branch.`);
|
|
118
98
|
};
|
|
119
99
|
/** The same config with the AI Gateway enabled, leaving any other `preview` entries intact. */
|
|
120
100
|
const withAiGateway = (config) => ({
|
|
121
|
-
|
|
122
|
-
|
|
101
|
+
...config,
|
|
102
|
+
preview: {
|
|
103
|
+
...config.preview,
|
|
104
|
+
aiGateway: true
|
|
105
|
+
}
|
|
123
106
|
});
|
|
124
107
|
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const resolveWithImpliedGateway = async (config, ctx,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// for a reason that has nothing to do with the feature, and this is not the place to
|
|
154
|
-
// guess which. Name both, and the command that answers it.
|
|
155
|
-
log.warning("Could not reach the AI Gateway's credentials, so %s were not resolved. Everything " +
|
|
156
|
-
"else was. Either this project does not have the AI Gateway, or the call failed — " +
|
|
157
|
-
`\`${getCliName()} env pull -s ai-gateway\` will say which.\nDetails: %s`, [
|
|
158
|
-
NEON_ENV_VAR_KEYS.aiGateway.apiKey,
|
|
159
|
-
NEON_ENV_VAR_KEYS.aiGateway.baseUrl,
|
|
160
|
-
].join(" and "), unreachable);
|
|
161
|
-
return {
|
|
162
|
-
...(await fetchAndProject(config, ctx)),
|
|
163
|
-
skipped: ["ai-gateway"],
|
|
164
|
-
};
|
|
108
|
+
* Tier-2 resolution with the AI Gateway added on top of the branch's read-back state.
|
|
109
|
+
*
|
|
110
|
+
* The gateway is not detectable — `pullConfig` reports no enabled flag for it — so it is
|
|
111
|
+
* implied rather than observed. Nobody named it, so it must never be the reason the whole
|
|
112
|
+
* resolve fails: a project outside the regions where branch credentials exist would otherwise
|
|
113
|
+
* lose its `DATABASE_URL` too, and `neon dev` would start with no env at all.
|
|
114
|
+
*
|
|
115
|
+
* So the gateway is only added once its credential endpoint has been shown to answer, by
|
|
116
|
+
* reading the branch's credentials first. A project that does not have them says so on a
|
|
117
|
+
* read, before anything is minted — which is the whole question, since the gateway's env is a
|
|
118
|
+
* credential and nothing else.
|
|
119
|
+
*
|
|
120
|
+
* Deciding this **before** resolving, rather than by catching and retrying, is what keeps it
|
|
121
|
+
* honest. A retry re-runs every call the first attempt made, so it would blame the gateway for
|
|
122
|
+
* a one-off failure in shared work, and — worse — a first attempt that minted a credential and
|
|
123
|
+
* then failed would be papered over by a second that succeeds without one, swallowing the
|
|
124
|
+
* error and stranding a secret nobody holds. Once the read succeeds, a later failure is a real
|
|
125
|
+
* failure and propagates: the same thing already happens on a branch with object storage,
|
|
126
|
+
* whose credential is minted whether or not the gateway is involved.
|
|
127
|
+
*/
|
|
128
|
+
const resolveWithImpliedGateway = async (config, ctx, branch) => {
|
|
129
|
+
const unreachable = await credentialsUnreachable(ctx, branch);
|
|
130
|
+
if (unreachable === null) return await fetchAndProject(withAiGateway(config), ctx);
|
|
131
|
+
log.warning(`Could not reach the AI Gateway's credentials, so %s were not resolved. Everything else was. Either this project does not have the AI Gateway, or the call failed — \`${getCliName()} env pull -s ai-gateway\` will say which.\nDetails: %s`, [NEON_ENV_VAR_KEYS.aiGateway.apiKey, NEON_ENV_VAR_KEYS.aiGateway.baseUrl].join(" and "), unreachable);
|
|
132
|
+
return {
|
|
133
|
+
...await fetchAndProject(config, ctx),
|
|
134
|
+
skipped: ["ai-gateway"]
|
|
135
|
+
};
|
|
165
136
|
};
|
|
166
137
|
/**
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
138
|
+
* Why the branch's credentials could not be read, or `null` when they could. A plain read: it
|
|
139
|
+
* mints nothing, revokes nothing, and changes nothing, so asking is free of the side effects
|
|
140
|
+
* that make a failed resolve ambiguous.
|
|
141
|
+
*/
|
|
171
142
|
const credentialsUnreachable = async (ctx, branch) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
143
|
+
try {
|
|
144
|
+
await apiFor(ctx).listCredentials(branch.projectId, branch.branchId);
|
|
145
|
+
return null;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return err instanceof Error ? err.message : String(err);
|
|
148
|
+
}
|
|
179
149
|
};
|
|
180
150
|
/** The adapter for direct branch reads: the injected one in tests, else built from options. */
|
|
181
|
-
const apiFor = (ctx) => ctx.api ??
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
151
|
+
const apiFor = (ctx) => ctx.api ?? createNeonApiFromOptions("neon env", {
|
|
152
|
+
...ctx.apiKey ? { apiKey: ctx.apiKey } : {},
|
|
153
|
+
...ctx.apiHost ? { apiHost: ctx.apiHost } : {}
|
|
154
|
+
});
|
|
186
155
|
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
156
|
+
* Tier-0: resolve exactly the services `--service` named, with `neon.ts` out of the picture.
|
|
157
|
+
*
|
|
158
|
+
* The selection is checked against the branch's live state so a service that is named but not
|
|
159
|
+
* provisioned fails by name, instead of quietly contributing no vars. `postgres` and the AI
|
|
160
|
+
* Gateway are not checked: every branch has Postgres, and the gateway has no branch-level
|
|
161
|
+
* state to check (an unavailable one surfaces when its credential is minted).
|
|
162
|
+
*/
|
|
194
163
|
const resolveSelectedServices = async (ctx, services) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
has("data-api") ? readDataApiEnabled(api, projectId, branchId) : null,
|
|
211
|
-
has("object-storage")
|
|
212
|
-
? api.listBranchBuckets(projectId, branchId)
|
|
213
|
-
: null,
|
|
214
|
-
]);
|
|
215
|
-
const config = configForServices(services, branchId, {
|
|
216
|
-
authEnabled: auth !== null,
|
|
217
|
-
dataApiEnabled,
|
|
218
|
-
buckets: buckets ?? [],
|
|
219
|
-
});
|
|
220
|
-
// A selection resolves part of the branch, so it must not revoke: the credential its
|
|
221
|
-
// persisted secrets name may also back a service it is not resolving. See
|
|
222
|
-
// `fetchEnvReusingSecrets`'s `revokeSuperseded`.
|
|
223
|
-
return await fetchAndProject(config, ctx, { revokeSuperseded: false });
|
|
164
|
+
const { projectId, branchId } = ctx;
|
|
165
|
+
if (!projectId || !branchId) throw new MissingBranchContextError(`--service needs a project and branch to read from. Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass --project-id / --branch.`);
|
|
166
|
+
const api = apiFor(ctx);
|
|
167
|
+
const has = (service) => services.includes(service);
|
|
168
|
+
const [auth, dataApiEnabled, buckets] = await Promise.all([
|
|
169
|
+
has("auth") ? api.getNeonAuth(projectId, branchId) : null,
|
|
170
|
+
has("data-api") ? readDataApiEnabled(api, projectId, branchId) : null,
|
|
171
|
+
has("object-storage") ? api.listBranchBuckets(projectId, branchId) : null
|
|
172
|
+
]);
|
|
173
|
+
const config = configForServices(services, branchId, {
|
|
174
|
+
authEnabled: auth !== null,
|
|
175
|
+
dataApiEnabled,
|
|
176
|
+
buckets: buckets ?? []
|
|
177
|
+
});
|
|
178
|
+
return await fetchAndProject(config, ctx, { revokeSuperseded: false });
|
|
224
179
|
};
|
|
225
180
|
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
181
|
+
* Whether the branch has a Data API integration — or `null` when that cannot be determined.
|
|
182
|
+
*
|
|
183
|
+
* It is enabled per branch *and database*, so this has to probe the database `fetchEnv` will
|
|
184
|
+
* resolve the URL from, or the two would disagree. That is Neon's default `neondb`, else the
|
|
185
|
+
* only database; several databases with no `neondb` is a case `fetchEnv` refuses to auto-pick
|
|
186
|
+
* at all. Reporting "no Data API integration" there would be a claim this read cannot support,
|
|
187
|
+
* so it answers `null` and lets `fetchEnv` raise its own ambiguity error, which names the
|
|
188
|
+
* databases and the fix.
|
|
189
|
+
*/
|
|
235
190
|
const readDataApiEnabled = async (api, projectId, branchId) => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return databases.length === 0 ? false : null;
|
|
241
|
-
const dataApi = await api.getNeonDataApi(projectId, branchId, database.name);
|
|
242
|
-
return dataApi !== null;
|
|
191
|
+
const databases = await api.listBranchDatabases(projectId, branchId);
|
|
192
|
+
const database = databases.find((db) => db.name === NEON_DEFAULT_DATABASE) ?? (databases.length === 1 ? databases[0] : void 0);
|
|
193
|
+
if (!database) return databases.length === 0 ? false : null;
|
|
194
|
+
return await api.getNeonDataApi(projectId, branchId, database.name) !== null;
|
|
243
195
|
};
|
|
244
196
|
/** Neon's default database, and the one `fetchEnv` prefers when a branch has several. */
|
|
245
197
|
const NEON_DEFAULT_DATABASE = "neondb";
|
|
246
198
|
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
199
|
+
* Build the `Config` an explicit `--service` selection stands for, raising
|
|
200
|
+
* {@link ServiceNotOnBranchError} for anything the branch does not have. Naming a service
|
|
201
|
+
* that isn't there has to fail rather than contribute no vars, or a scoped pull would report
|
|
202
|
+
* "no Neon env variables to pull" — which reads as a statement about the branch rather than
|
|
203
|
+
* about the selection.
|
|
204
|
+
*/
|
|
253
205
|
const configForServices = (services, branchId, branch) => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const preview = {};
|
|
280
|
-
if (services.includes("object-storage")) {
|
|
281
|
-
if (branch.buckets.length === 0) {
|
|
282
|
-
notOnBranch("object-storage", "object-storage buckets");
|
|
283
|
-
}
|
|
284
|
-
preview.buckets = Object.fromEntries(branch.buckets.map((bucket) => [
|
|
285
|
-
bucket.name,
|
|
286
|
-
{ access: bucket.accessLevel },
|
|
287
|
-
]));
|
|
288
|
-
}
|
|
289
|
-
if (services.includes("ai-gateway"))
|
|
290
|
-
preview.aiGateway = true;
|
|
291
|
-
if (Object.keys(preview).length > 0)
|
|
292
|
-
config.preview = preview;
|
|
293
|
-
return config;
|
|
206
|
+
const provisionWith = {
|
|
207
|
+
auth: `${getCliName()} neon-auth enable`,
|
|
208
|
+
"data-api": `${getCliName()} data-api create`,
|
|
209
|
+
"object-storage": `${getCliName()} buckets create <name>`
|
|
210
|
+
};
|
|
211
|
+
const notOnBranch = (service, what) => {
|
|
212
|
+
throw new ServiceNotOnBranchError(`--service ${service}: branch ${branchId} has no ${what}, so there are no ${service} env vars to pull. Provision it first (\`${provisionWith[service]}\`, or in the Neon Console), or drop ${service} from --service.`);
|
|
213
|
+
};
|
|
214
|
+
const config = {};
|
|
215
|
+
if (services.includes("auth")) {
|
|
216
|
+
if (!branch.authEnabled) notOnBranch("auth", "Neon Auth integration");
|
|
217
|
+
config.auth = true;
|
|
218
|
+
}
|
|
219
|
+
if (services.includes("data-api")) {
|
|
220
|
+
if (branch.dataApiEnabled === false) notOnBranch("data-api", "Data API integration");
|
|
221
|
+
config.dataApi = true;
|
|
222
|
+
}
|
|
223
|
+
const preview = {};
|
|
224
|
+
if (services.includes("object-storage")) {
|
|
225
|
+
if (branch.buckets.length === 0) notOnBranch("object-storage", "object-storage buckets");
|
|
226
|
+
preview.buckets = Object.fromEntries(branch.buckets.map((bucket) => [bucket.name, { access: bucket.accessLevel }]));
|
|
227
|
+
}
|
|
228
|
+
if (services.includes("ai-gateway")) preview.aiGateway = true;
|
|
229
|
+
if (Object.keys(preview).length > 0) config.preview = preview;
|
|
230
|
+
return config;
|
|
294
231
|
};
|
|
295
232
|
/**
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
reason: `could not reach Neon (${detail}); running without Neon env`,
|
|
330
|
-
},
|
|
331
|
-
};
|
|
332
|
-
}
|
|
233
|
+
* `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
|
|
234
|
+
*
|
|
235
|
+
* - Success → `{ vars }` (possibly just the always-present Postgres URLs).
|
|
236
|
+
* - No linked branch / project → `{ vars: {}, skipped }` with a "link a branch" hint; the
|
|
237
|
+
* function still runs locally, just without Neon env.
|
|
238
|
+
* - Any other failure (offline, transient API error) → `{ vars: {}, skipped }` naming the
|
|
239
|
+
* cause; again non-fatal.
|
|
240
|
+
* - {@link DevEnvMismatchError} (policy declares a secret-bearing service the branch lacks)
|
|
241
|
+
* is the one hard stop and is re-thrown for the caller to surface.
|
|
242
|
+
*/
|
|
243
|
+
const resolveDevEnv = async (ctx) => {
|
|
244
|
+
try {
|
|
245
|
+
const { vars, credential } = await resolveNeonEnvVars(ctx);
|
|
246
|
+
return {
|
|
247
|
+
vars,
|
|
248
|
+
credential
|
|
249
|
+
};
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err instanceof DevEnvMismatchError) throw err;
|
|
252
|
+
if (err instanceof MissingBranchContextError) {
|
|
253
|
+
log.debug("dev: %s; skipping env injection", err.message);
|
|
254
|
+
return {
|
|
255
|
+
vars: {},
|
|
256
|
+
skipped: { reason: `no linked Neon branch — run \`${getCliName()} link\`, then \`${getCliName()} checkout <branch>\`, to inject DATABASE_URL and friends` }
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
260
|
+
log.debug("dev: env resolution failed: %s", detail);
|
|
261
|
+
return {
|
|
262
|
+
vars: {},
|
|
263
|
+
skipped: { reason: `could not reach Neon (${detail}); running without Neon env` }
|
|
264
|
+
};
|
|
265
|
+
}
|
|
333
266
|
};
|
|
334
267
|
/**
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
268
|
+
* Return the policy with its `preview.functions` removed, so the env path never enumerates
|
|
269
|
+
* functions against the Neon API. Functions are local-source-bundled and produce no
|
|
270
|
+
* branch-level secrets, so they are irrelevant to env resolution; probing them only risks
|
|
271
|
+
* failing the whole resolve (undeployed function, or Functions Preview disabled on the
|
|
272
|
+
* project). Buckets / AI Gateway and the top-level Auth / Data API toggles are preserved —
|
|
273
|
+
* they DO carry env, so they must still be checked and resolved. Returns the config
|
|
274
|
+
* unchanged when it declares no functions.
|
|
275
|
+
*/
|
|
343
276
|
const withoutPreviewFunctions = (config) => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
277
|
+
const preview = config.preview;
|
|
278
|
+
if (!preview?.functions) return config;
|
|
279
|
+
const previewWithoutFunctions = { ...preview };
|
|
280
|
+
delete previewWithoutFunctions.functions;
|
|
281
|
+
return {
|
|
282
|
+
...config,
|
|
283
|
+
preview: previewWithoutFunctions
|
|
284
|
+
};
|
|
350
285
|
};
|
|
351
286
|
/**
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
287
|
+
* Tier-1 guard. Dry-run the policy against the branch's live state and stop if
|
|
288
|
+
* it declares a branch-level resource the branch is missing. Built on `plan` so
|
|
289
|
+
* it covers every present and future provisionable resource for free: any
|
|
290
|
+
* `create` action is a resource `neonctl deploy` would provision.
|
|
291
|
+
*
|
|
292
|
+
* Called with functions already stripped (see {@link withoutPreviewFunctions}), so the
|
|
293
|
+
* `plan` probe never enumerates the functions API — an undeployed function, or a project
|
|
294
|
+
* without the Functions Preview, must never block local dev or sink env injection.
|
|
295
|
+
*/
|
|
361
296
|
const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return;
|
|
370
|
-
const names = missing.map((change) => change.identifier).join(", ");
|
|
371
|
-
throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
|
|
372
|
-
"does not have it yet, so the matching env vars cannot be injected. " +
|
|
373
|
-
`Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), ` +
|
|
374
|
-
`then re-run \`${getCliName()} dev\`.`);
|
|
297
|
+
const missing = (await plan(config, {
|
|
298
|
+
projectId: ctx.projectId,
|
|
299
|
+
branchId: ctx.branchId,
|
|
300
|
+
...apiOptions(ctx)
|
|
301
|
+
})).applied.filter(isMissingResource);
|
|
302
|
+
if (missing.length === 0) return;
|
|
303
|
+
throw new DevEnvMismatchError(`Your neon.ts declares ${missing.map((change) => change.identifier).join(", ")} for branch ${ctx.branchId}, but the branch does not have it yet, so the matching env vars cannot be injected. Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), then re-run \`${getCliName()} dev\`.`);
|
|
375
304
|
};
|
|
376
305
|
/**
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const isMissingResource = (change) => change.kind === "service" &&
|
|
383
|
-
change.action === "create" &&
|
|
384
|
-
!change.identifier.startsWith("function:");
|
|
306
|
+
* A planned change that provisions a branch-level resource the branch lacks: a
|
|
307
|
+
* `create` on a service (Neon Auth, Data API, a bucket, the AI Gateway). Branch
|
|
308
|
+
* setting drift (`update`) and `noop`s are ignored — they don't block local dev
|
|
309
|
+
* — and functions are excluded (see {@link assertPolicyMatchesBranch}).
|
|
310
|
+
*/
|
|
311
|
+
const isMissingResource = (change) => change.kind === "service" && change.action === "create" && !change.identifier.startsWith("function:");
|
|
385
312
|
const fetchAndProject = async (config, ctx, opts = {}) => fetchEnvReusingSecrets(config, {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
313
|
+
projectId: ctx.projectId,
|
|
314
|
+
branch: ctx.branchId,
|
|
315
|
+
...apiOptions(ctx),
|
|
316
|
+
...ctx.env ? { env: ctx.env } : {},
|
|
317
|
+
...opts.revokeSuperseded === false ? { revokeSuperseded: false } : {}
|
|
391
318
|
});
|
|
392
319
|
/**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
320
|
+
* Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
|
|
321
|
+
* root. Returns `null` when there is none (the common "no config" case), and
|
|
322
|
+
* surfaces real load errors (e.g. a syntax error in an existing file).
|
|
323
|
+
*/
|
|
397
324
|
/**
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
325
|
+
* Substrings that mark a module-resolution failure while loading `neon.ts` —
|
|
326
|
+
* almost always because the project's dependencies aren't installed yet (the
|
|
327
|
+
* config imports `@neon/config` & friends). Deliberately specific:
|
|
328
|
+
* the generic "…or a missing dependency…" hint the loader always appends is
|
|
329
|
+
* NOT in here, so a real syntax/runtime error doesn't get mislabeled.
|
|
330
|
+
*/
|
|
404
331
|
const MISSING_DEPENDENCY_HINTS = [
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
332
|
+
"cannot find module",
|
|
333
|
+
"cannot find package",
|
|
334
|
+
"err_module_not_found",
|
|
335
|
+
"failed to resolve",
|
|
336
|
+
"could not resolve",
|
|
337
|
+
"module not found"
|
|
411
338
|
];
|
|
412
339
|
/** Flatten an error and its `cause` chain to one lowercased string for matching. */
|
|
413
340
|
const errorChainText = (err) => {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
341
|
+
const parts = [];
|
|
342
|
+
let current = err;
|
|
343
|
+
for (let depth = 0; current instanceof Error && depth < 6; depth++) {
|
|
344
|
+
parts.push(current.message);
|
|
345
|
+
current = current.cause;
|
|
346
|
+
}
|
|
347
|
+
return parts.join("\n").toLowerCase();
|
|
421
348
|
};
|
|
422
349
|
const looksLikeMissingDependency = (err) => {
|
|
423
|
-
|
|
424
|
-
|
|
350
|
+
const text = errorChainText(err);
|
|
351
|
+
return MISSING_DEPENDENCY_HINTS.some((hint) => text.includes(hint));
|
|
425
352
|
};
|
|
426
353
|
const loadNeonConfig = async (cwd) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// A neon.ts that imports a package which isn't installed fails here with a
|
|
437
|
-
// cryptic "Cannot find module …". Turn that into the actionable thing to do.
|
|
438
|
-
if (looksLikeMissingDependency(err)) {
|
|
439
|
-
throw new Error("Could not load neon.ts: a package it imports is not installed. " +
|
|
440
|
-
"Did you run `npm install`? Install your dependencies " +
|
|
441
|
-
"(npm / pnpm / yarn / bun), then try again.\n" +
|
|
442
|
-
`Original error: ${message}`);
|
|
443
|
-
}
|
|
444
|
-
throw err;
|
|
445
|
-
}
|
|
354
|
+
try {
|
|
355
|
+
const { config } = await loadConfigFromFile({ cwd });
|
|
356
|
+
return config;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
359
|
+
if (/Could not find a Neon config file/i.test(message)) return null;
|
|
360
|
+
if (looksLikeMissingDependency(err)) throw new Error(`Could not load neon.ts: a package it imports is not installed. Run \`${formatInstallCommand(resolvePackageManager(cwd))}\`, then try again.\nOriginal error: ${message}`);
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
446
363
|
};
|
|
364
|
+
//#endregion
|
|
365
|
+
export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, resolveDevEnv, resolveNeonEnvVars };
|