neon 8.11.0 → 8.11.2
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 +1 -1
- package/dist/_chunks/api-GbEaBZJ5.js +388 -0
- package/dist/_chunks/api_keys-CNnnugbQ.js +281 -0
- package/dist/_chunks/ask-DVwy0eUL.js +155 -0
- package/dist/_chunks/auth-CIUtXByB.js +495 -0
- package/dist/_chunks/bootstrap-wPZWa1sT.js +499 -0
- package/dist/_chunks/branches-BNa1xX1L.js +460 -0
- package/dist/_chunks/bucket-BCdryTkD.js +409 -0
- package/dist/_chunks/checkout-CXIOydqZ.js +348 -0
- package/dist/_chunks/claim-DqAvAvDm.js +473 -0
- package/dist/_chunks/config-BH_qRHiG.js +1113 -0
- package/dist/_chunks/connection_string-DB4awPzZ.js +154 -0
- package/dist/_chunks/credentials-BSjE9fRR.js +146 -0
- package/dist/_chunks/data_api-Djxj5ZQD.js +263 -0
- package/dist/_chunks/databases-Dok4wru7.js +84 -0
- package/dist/_chunks/deploy-D7B0Xfjg.js +33 -0
- package/dist/_chunks/dev-CSvxAecE.js +945 -0
- package/dist/_chunks/diff-DhxxbdQE.js +178 -0
- package/dist/_chunks/docs-CrVybkTP.js +197 -0
- package/dist/_chunks/env-5HS5qQGd.js +284 -0
- package/dist/_chunks/feedback-CMayyTa8.js +88 -0
- package/dist/_chunks/functions-Q0zuvtnk.js +464 -0
- package/dist/_chunks/git-H32TNt6M.js +356 -0
- package/dist/_chunks/init-BkA6RilG.js +111 -0
- package/dist/_chunks/inspect-g0wekylj.js +90 -0
- package/dist/_chunks/ip_allow-5lLFvIuE.js +106 -0
- package/dist/_chunks/link-BgtkOBws.js +936 -0
- package/dist/_chunks/logs-DWkpjzIH.js +291 -0
- package/dist/_chunks/mcp-Bo-7TKc7.js +253 -0
- package/dist/_chunks/neon_auth-CBs1_3qu.js +822 -0
- package/dist/_chunks/open-CX3BT3Tp.js +33 -0
- package/dist/_chunks/operations-D4I-AxVs.js +37 -0
- package/dist/_chunks/orgs-xGfANwHQ.js +52 -0
- package/dist/_chunks/plugins-DqD7BDO1.js +169 -0
- package/dist/_chunks/profile-yXOiKk9R.js +672 -0
- package/dist/_chunks/projects-BwI2Uo0z.js +439 -0
- package/dist/_chunks/psql-BM-doe8e.js +64 -0
- package/dist/_chunks/realtime-BRqIyGR-.js +79 -0
- package/dist/_chunks/roles-B4w3YT_7.js +79 -0
- package/dist/_chunks/set_context-DbrwoImy.js +38 -0
- package/dist/_chunks/skills-DyiBXKS3.js +255 -0
- package/dist/_chunks/snapshots-RJBzW8GM.js +338 -0
- package/dist/_chunks/status-DV8Z2kw3.js +44 -0
- package/dist/_chunks/triggers-BrRo1z4b.js +302 -0
- package/dist/_chunks/user-dlOGXbX9.js +60 -0
- package/dist/_chunks/vpc_endpoints-CNb0YfMo.js +105 -0
- package/dist/auth_recovery.js +1 -1
- package/dist/commands/api.js +1 -388
- package/dist/commands/api_keys.js +1 -281
- package/dist/commands/ask.js +1 -155
- package/dist/commands/auth.js +1 -495
- package/dist/commands/bootstrap.js +1 -499
- package/dist/commands/branches.js +1 -460
- package/dist/commands/bucket.js +1 -409
- package/dist/commands/checkout.js +1 -348
- package/dist/commands/claim.js +1 -473
- package/dist/commands/config.js +1 -1113
- package/dist/commands/connection_string.js +1 -154
- package/dist/commands/credentials.js +1 -146
- package/dist/commands/data_api.js +1 -263
- package/dist/commands/databases.js +1 -84
- package/dist/commands/deploy.js +1 -33
- package/dist/commands/dev.js +1 -945
- package/dist/commands/diff.js +1 -178
- package/dist/commands/docs.js +1 -197
- package/dist/commands/env.js +1 -284
- package/dist/commands/feedback.js +1 -88
- package/dist/commands/functions.js +1 -464
- package/dist/commands/functions_new.js +6 -6
- package/dist/commands/git.js +1 -356
- package/dist/commands/index.js +45 -45
- package/dist/commands/init.js +1 -111
- package/dist/commands/inspect.js +1 -90
- package/dist/commands/ip_allow.js +1 -106
- package/dist/commands/link.js +1 -936
- package/dist/commands/logs.js +1 -291
- package/dist/commands/mcp.js +1 -253
- package/dist/commands/neon_auth.js +1 -822
- package/dist/commands/open.js +1 -33
- package/dist/commands/operations.js +1 -37
- package/dist/commands/orgs.js +1 -52
- package/dist/commands/plugins.js +1 -169
- package/dist/commands/profile.js +1 -672
- package/dist/commands/projects.js +1 -439
- package/dist/commands/psql.js +1 -64
- package/dist/commands/realtime.js +1 -79
- package/dist/commands/roles.js +1 -79
- package/dist/commands/set_context.js +1 -38
- package/dist/commands/skills.js +1 -255
- package/dist/commands/snapshots.js +1 -338
- package/dist/commands/status.js +1 -44
- package/dist/commands/subscribe.js +1 -1
- package/dist/commands/triggers.js +1 -302
- package/dist/commands/user.js +1 -60
- package/dist/commands/vpc_endpoints.js +1 -105
- package/dist/functions/registry.js +29 -151
- package/dist/functions/source.js +42 -2
- package/dist/index.js +1 -1
- package/dist/init/auth.js +3 -3
- package/dist/init/link.js +2 -2
- package/dist/init/run.js +3 -3
- package/dist/init/tooling.js +3 -3
- package/dist/mcp/mint.js +1 -1
- package/dist/test_utils/registry.js +1 -2
- package/dist/utils/package_manager.js +1 -0
- package/package.json +4 -2
package/dist/commands/config.js
CHANGED
|
@@ -1,1113 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { isCi, isInteractiveTerminal } from "../env.js";
|
|
3
|
-
import { log } from "../log.js";
|
|
4
|
-
import { getApiClient } from "../api.js";
|
|
5
|
-
import { contextBranch, ensureDirectoryGitignored, readContextFile } from "../context.js";
|
|
6
|
-
import { getCliName } from "../utils/cli_name.js";
|
|
7
|
-
import { NEON_SERVICES, NEON_SERVICE_LABELS, deprecatedServiceMessage, servicesFlagValue, servicesOption } from "../neon_services.js";
|
|
8
|
-
import { FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, parseConfigInitServices, renderFunctionSource, renderNeonConfig, renderNeonConfigFromView } from "../config_template.js";
|
|
9
|
-
import { TYPESCRIPT_EXTENSION, asRelativeModulePath, editNeonConfig } from "../config_edit.js";
|
|
10
|
-
import { toNeonConfigView } from "../config_format.js";
|
|
11
|
-
import { declaredNeonServices } from "../config_services.js";
|
|
12
|
-
import { loadProjectConfig, neonConfigFilename, projectConfigPath, projectDir } from "../project.js";
|
|
13
|
-
import { loadEnvFileIntoProcess } from "../env_file.js";
|
|
14
|
-
import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
|
|
15
|
-
import { writer } from "../writer.js";
|
|
16
|
-
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
17
|
-
import { assertAiGatewayProvisionable, warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
18
|
-
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
19
|
-
import { renderAppliedChanges, renderBranchSettingConflicts, renderCustomDomainFollowup } from "../utils/config_diff.js";
|
|
20
|
-
import { bundleEntry } from "../utils/esbuild.js";
|
|
21
|
-
import { FUNCTION_SLUG_PATTERN, deriveFunctionSlug } from "../utils/function_slug.js";
|
|
22
|
-
import { readGitContext } from "../utils/git.js";
|
|
23
|
-
import { resolveHookEnv, runDeployAfterHook, runDeployBeforeHook } from "../utils/hooks.js";
|
|
24
|
-
import { pickServicesInteractively } from "../utils/service_picker.js";
|
|
25
|
-
import { zipBundle } from "../utils/zip.js";
|
|
26
|
-
import { autoPullEnvAfterPin } from "./env.js";
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
29
|
-
import chalk from "chalk";
|
|
30
|
-
import { defineConfig, packagesToStage, previewGaWarningForConfig, resolveConfig } from "@neon/config";
|
|
31
|
-
import { PushAbortedError, PushConflictError, apply, assertZipWithinLimits, bundleAsIs, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, plan, resolveEsbuildEntry, traceNativePackages, zipFunctionBundle } from "@neon/config-runtime";
|
|
32
|
-
//#region src/commands/config.ts
|
|
33
|
-
var config_exports = /* @__PURE__ */ __exportAll({
|
|
34
|
-
ConfigInstallFailed: () => ConfigInstallFailed,
|
|
35
|
-
addCmd: () => addCmd,
|
|
36
|
-
applyCmd: () => applyCmd,
|
|
37
|
-
applyFlags: () => applyFlags,
|
|
38
|
-
applyPolicyOnCreate: () => applyPolicyOnCreate,
|
|
39
|
-
builder: () => builder,
|
|
40
|
-
command: () => command,
|
|
41
|
-
createBranchFromPolicyOnCheckout: () => createBranchFromPolicyOnCheckout,
|
|
42
|
-
describe: () => describe,
|
|
43
|
-
ensureConfigPackages: () => ensureConfigPackages,
|
|
44
|
-
envFlag: () => envFlag,
|
|
45
|
-
envPullFlag: () => envPullFlag,
|
|
46
|
-
handler: () => handler,
|
|
47
|
-
initCmd: () => initCmd,
|
|
48
|
-
installCommandFrom: () => installCommandFrom,
|
|
49
|
-
missingDependencies: () => missingDependencies,
|
|
50
|
-
planCmd: () => planCmd,
|
|
51
|
-
status: () => status
|
|
52
|
-
});
|
|
53
|
-
const neonctlBundler = async (fn) => {
|
|
54
|
-
if (typeof fn.bundler === "function") return zipFunctionBundle(fn.slug, await fn.bundler(fn));
|
|
55
|
-
if (fn.bundler === "none") return zipFunctionBundle(fn.slug, await bundleAsIs(fn));
|
|
56
|
-
const entry = await resolveEsbuildEntry(fn.source);
|
|
57
|
-
const externalPackages = fn.externalPackages ?? [];
|
|
58
|
-
const { files, metafile, warnings } = await bundleEntry(entry, { externalPackages: externalPackages.map((pkg) => pkg.name) });
|
|
59
|
-
for (const warning of warnings) log.warning(warning);
|
|
60
|
-
for (const finding of findUndeclaredNativePackages({
|
|
61
|
-
metafile,
|
|
62
|
-
declared: externalPackages.map((pkg) => pkg.name),
|
|
63
|
-
projectDir: dirname(entry)
|
|
64
|
-
})) log.warning(describeNativeFinding(fn.slug, finding));
|
|
65
|
-
const staged = packagesToStage(externalPackages);
|
|
66
|
-
if (staged.length === 0) return zipBundle(files);
|
|
67
|
-
const traced = await traceNativePackages({
|
|
68
|
-
slug: fn.slug,
|
|
69
|
-
packages: staged,
|
|
70
|
-
projectDir: dirname(entry)
|
|
71
|
-
});
|
|
72
|
-
for (const warning of traced.warnings) log.warning(warning);
|
|
73
|
-
const entries = {
|
|
74
|
-
...files,
|
|
75
|
-
...traced.entries
|
|
76
|
-
};
|
|
77
|
-
enforceLimits(fn.slug, entries);
|
|
78
|
-
const zip = zipBundle(entries);
|
|
79
|
-
assertZipWithinLimits(fn.slug, zip, entries);
|
|
80
|
-
return zip;
|
|
81
|
-
};
|
|
82
|
-
const reasonAllowsUpdateExisting = (reason) => reason.includes("Pass `updateExisting: true`");
|
|
83
|
-
const INSPECT_FIELDS = [
|
|
84
|
-
"project",
|
|
85
|
-
"branch",
|
|
86
|
-
"config"
|
|
87
|
-
];
|
|
88
|
-
const envFlag = { env: {
|
|
89
|
-
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. Function env values that read process.env must be set in this file or the environment.",
|
|
90
|
-
type: "string"
|
|
91
|
-
} };
|
|
92
|
-
/** Apply-only flags, exported so `deploy` can reuse the exact same surface. */
|
|
93
|
-
const applyFlags = {
|
|
94
|
-
"update-existing": {
|
|
95
|
-
describe: "Override branch settings that differ from neon.ts without asking",
|
|
96
|
-
type: "boolean",
|
|
97
|
-
default: false
|
|
98
|
-
},
|
|
99
|
-
"allow-protected": {
|
|
100
|
-
describe: "Apply to a branch marked protected on Neon without asking",
|
|
101
|
-
type: "boolean",
|
|
102
|
-
default: false
|
|
103
|
-
},
|
|
104
|
-
yes: {
|
|
105
|
-
alias: "y",
|
|
106
|
-
describe: "Answer yes to every confirmation: --update-existing and --allow-protected",
|
|
107
|
-
type: "boolean",
|
|
108
|
-
default: false
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* `--env-pull` for `config apply` / `deploy` (shared so both expose the identical surface).
|
|
113
|
-
* After a successful apply, the branch's Neon env vars are written to a local `.env` — the
|
|
114
|
-
* same bundled convenience as `link` / `checkout`. On by default; `--no-env-pull` opts out.
|
|
115
|
-
*/
|
|
116
|
-
const envPullFlag = { "env-pull": {
|
|
117
|
-
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`).",
|
|
118
|
-
type: "boolean",
|
|
119
|
-
default: true
|
|
120
|
-
} };
|
|
121
|
-
/** package.json fields a dependency can be declared in. */
|
|
122
|
-
const DEPENDENCY_FIELDS = [
|
|
123
|
-
"dependencies",
|
|
124
|
-
"devDependencies",
|
|
125
|
-
"peerDependencies",
|
|
126
|
-
"optionalDependencies"
|
|
127
|
-
];
|
|
128
|
-
var ConfigInstallFailed = class extends Error {
|
|
129
|
-
constructor(command) {
|
|
130
|
-
super("Could not install the Neon dependencies.");
|
|
131
|
-
this.name = "ConfigInstallFailed";
|
|
132
|
-
this.command = command;
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
136
|
-
/**
|
|
137
|
-
* The {@link REQUIRED_PACKAGES} not already declared in the project's package.json
|
|
138
|
-
* (any dependency field). A missing or malformed package.json means none are
|
|
139
|
-
* declared, so all are reported missing.
|
|
140
|
-
*/
|
|
141
|
-
const missingDependencies = (cwd) => {
|
|
142
|
-
const declared = /* @__PURE__ */ new Set();
|
|
143
|
-
const pkgPath = join(cwd, "package.json");
|
|
144
|
-
if (existsSync(pkgPath)) {
|
|
145
|
-
let parsed;
|
|
146
|
-
try {
|
|
147
|
-
parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
148
|
-
} catch {
|
|
149
|
-
parsed = void 0;
|
|
150
|
-
}
|
|
151
|
-
if (isRecord(parsed)) for (const field of DEPENDENCY_FIELDS) {
|
|
152
|
-
const deps = parsed[field];
|
|
153
|
-
if (isRecord(deps)) for (const name of Object.keys(deps)) declared.add(name);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return REQUIRED_PACKAGES.filter((pkg) => !declared.has(pkg));
|
|
157
|
-
};
|
|
158
|
-
/**
|
|
159
|
-
* Which services the scaffolded policy declares. `--services` always wins so a script or an
|
|
160
|
-
* agent gets the same result without a TTY; an injected picker is next (tests); the real
|
|
161
|
-
* picker only runs on an interactive terminal outside CI. Everything else scaffolds the
|
|
162
|
-
* starter policy, which is what `config init` has always written.
|
|
163
|
-
*/
|
|
164
|
-
const resolveServices = async (props) => {
|
|
165
|
-
if (props.services !== void 0) return parseConfigInitServices(props.services, (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical)));
|
|
166
|
-
if (props.pickServices) return props.pickServices();
|
|
167
|
-
if (isCi() || !process.stdout.isTTY) return [];
|
|
168
|
-
return pickServicesInteractively();
|
|
169
|
-
};
|
|
170
|
-
/**
|
|
171
|
-
* Write the hello-world handler the scaffolded `functions` entry points at. An
|
|
172
|
-
* existing `hello.ts` is left alone: the declared function keeps pointing at it, which is the
|
|
173
|
-
* better outcome than overwriting a file the user wrote.
|
|
174
|
-
*/
|
|
175
|
-
const scaffoldFunction = (cwd) => {
|
|
176
|
-
const path = join(cwd, FUNCTION_FILENAME);
|
|
177
|
-
if (existsSync(path)) {
|
|
178
|
-
log.info("Found an existing %s — leaving it untouched; the %s function points at it.", FUNCTION_FILENAME, FUNCTION_SLUG);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
writeFileSync(path, FUNCTION_TEMPLATE);
|
|
182
|
-
log.info("Created %s — the source of the %s function.", FUNCTION_FILENAME, FUNCTION_SLUG);
|
|
183
|
-
};
|
|
184
|
-
/**
|
|
185
|
-
* Read a branch's live state and render it as a `neon.ts`. The read goes through the same
|
|
186
|
-
* {@link liveConfigView} as `config status`, so a seeded policy declares exactly what
|
|
187
|
-
* `config status --config-json` reports — including what it cannot report (see
|
|
188
|
-
* {@link renderNeonConfigFromView}).
|
|
189
|
-
*/
|
|
190
|
-
const seedFromBranch = async (props) => {
|
|
191
|
-
const { apiClient, projectId } = props;
|
|
192
|
-
if (!apiClient || !projectId) throw new Error("--from-branch needs a project. Pass --project-id, or run `neon link` to pin one in .neon.");
|
|
193
|
-
const ref = await resolveBranchRef({
|
|
194
|
-
apiClient,
|
|
195
|
-
projectId,
|
|
196
|
-
...props.branch !== void 0 ? { branch: props.branch } : {}
|
|
197
|
-
});
|
|
198
|
-
if (ref.usedDefault) log.info("No branch pinned or passed — seeding from the project's default branch %s.", ref.branchName);
|
|
199
|
-
const { live, view } = await liveConfigView({
|
|
200
|
-
projectId,
|
|
201
|
-
branchId: ref.branchId,
|
|
202
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
203
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
204
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
|
|
205
|
-
});
|
|
206
|
-
return {
|
|
207
|
-
...renderNeonConfigFromView(view, live.branch.name),
|
|
208
|
-
branchName: live.branch.name
|
|
209
|
-
};
|
|
210
|
-
};
|
|
211
|
-
/** The install command to print, prefixed with a `cd` when the project is not the cwd. */
|
|
212
|
-
const installCommandFrom = (dir, pm, missing) => {
|
|
213
|
-
const command = formatInstallCommand(pm, [...missing]);
|
|
214
|
-
const rel = relative(process.cwd(), dir);
|
|
215
|
-
if (rel === "") return command;
|
|
216
|
-
const target = rel.length <= dir.length ? rel : dir;
|
|
217
|
-
return `cd ${shellQuote(target)} && ${command}`;
|
|
218
|
-
};
|
|
219
|
-
/** Install the config packages the project is missing, or say how to. */
|
|
220
|
-
const ensureConfigPackages = async (cwd, props) => {
|
|
221
|
-
const run = props.run ?? runCommand;
|
|
222
|
-
const missing = missingDependencies(cwd);
|
|
223
|
-
if (missing.length === 0) {
|
|
224
|
-
if (!props.silent) log.info("%s are already installed.", REQUIRED_PACKAGES.join(" and "));
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const pm = props.packageManager ?? resolvePackageManager(cwd);
|
|
228
|
-
const args = installArgs(pm, missing);
|
|
229
|
-
const installHint = installCommandFrom(cwd, pm, missing);
|
|
230
|
-
if (props.install === false) {
|
|
231
|
-
if (!props.silent) log.info("Install the Neon config packages to use neon.ts: %s", installHint);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const pkgPath = join(cwd, "package.json");
|
|
235
|
-
if (!existsSync(pkgPath)) {
|
|
236
|
-
writeFileSync(pkgPath, "{}\n");
|
|
237
|
-
if (!props.silent) log.info("Created package.json.");
|
|
238
|
-
}
|
|
239
|
-
ensureDirectoryGitignored(join(cwd, "node_modules"));
|
|
240
|
-
if (!props.silent) log.info("Installing %s with %s…", missing.join(", "), pm);
|
|
241
|
-
if (!await run(pm, args, cwd)) {
|
|
242
|
-
if (props.requireInstall) throw new ConfigInstallFailed(installHint);
|
|
243
|
-
log.warning("Could not install the config packages automatically. Run by hand: %s", installHint);
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
/**
|
|
247
|
-
* Scaffold a `neon.ts` policy and make sure the Neon config packages are
|
|
248
|
-
* installed, so a project can go straight to `neon config plan` / `apply`.
|
|
249
|
-
* Local-only unless `--from-branch` is set (see {@link isConfigInit}).
|
|
250
|
-
*/
|
|
251
|
-
const initCmd = async (props) => {
|
|
252
|
-
const cwd = props.cwd ?? projectDir(props.contextFile ? { contextFile: props.contextFile } : {});
|
|
253
|
-
const existing = neonConfigFilename(cwd);
|
|
254
|
-
if (existing) {
|
|
255
|
-
if (!props.silent) {
|
|
256
|
-
log.info("Found an existing %s — leaving it untouched.", existing);
|
|
257
|
-
if (props.services !== void 0) log.warning("--services was ignored. To declare services in the existing %s, use `%s config add`.", existing, getCliName());
|
|
258
|
-
}
|
|
259
|
-
} else if (props.fromBranch) {
|
|
260
|
-
const { source, seeded, branchName } = await seedFromBranch(props);
|
|
261
|
-
writeFileSync(join(cwd, "neon.ts"), source);
|
|
262
|
-
if (!props.silent) {
|
|
263
|
-
if (seeded) log.info("Created neon.ts from the live state of %s.", branchName);
|
|
264
|
-
else log.info("%s declares no services and no branch settings — created neon.ts with the starter policy instead.", branchName);
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
const services = await resolveServices(props);
|
|
268
|
-
writeFileSync(join(cwd, "neon.ts"), renderNeonConfig(services));
|
|
269
|
-
if (!props.silent) {
|
|
270
|
-
if (services.length === 0) log.info("Created neon.ts with a starter policy.");
|
|
271
|
-
else {
|
|
272
|
-
const declared = services.map((service) => service === "realtime" ? "Realtime" : service);
|
|
273
|
-
log.info("Created neon.ts declaring %s.", declared.join(", "));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (services.includes("functions")) scaffoldFunction(cwd);
|
|
277
|
-
}
|
|
278
|
-
await ensureConfigPackages(cwd, props);
|
|
279
|
-
if (!props.silent) log.info("Next: edit neon.ts, then run `neon config plan` to preview and `neon config apply`.");
|
|
280
|
-
};
|
|
281
|
-
const SCRIPT_EXTENSION = /\.(?:ts|mts|cts|js|mjs|cjs)$/;
|
|
282
|
-
const shellQuote = (value) => /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
283
|
-
/**
|
|
284
|
-
* `explicitConfig` is the `--config` the user passed. The commands suggested afterwards must
|
|
285
|
-
* carry it, or they would act on the project directory's `neon.ts` instead.
|
|
286
|
-
*/
|
|
287
|
-
const planAdd = (target, configPath, explicitConfig) => {
|
|
288
|
-
const language = TYPESCRIPT_EXTENSION.test(configPath) ? "ts" : "js";
|
|
289
|
-
const cli = getCliName();
|
|
290
|
-
const flag = explicitConfig ? ` --config ${shellQuote(explicitConfig)}` : "";
|
|
291
|
-
const provision = `Next: \`${cli} config plan${flag}\` to preview, \`${cli} config apply${flag}\` to provision.`;
|
|
292
|
-
switch (target.kind) {
|
|
293
|
-
case "realtime": {
|
|
294
|
-
const realtime = target.allowedOrigins === void 0 ? true : { allowedOrigins: target.allowedOrigins };
|
|
295
|
-
defineConfig({ preview: { realtime } });
|
|
296
|
-
return {
|
|
297
|
-
edit: {
|
|
298
|
-
kind: "realtime",
|
|
299
|
-
enabled: true,
|
|
300
|
-
...target.allowedOrigins !== void 0 ? { allowedOrigins: target.allowedOrigins } : {}
|
|
301
|
-
},
|
|
302
|
-
next: provision
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
case "service": return {
|
|
306
|
-
edit: {
|
|
307
|
-
kind: "service",
|
|
308
|
-
service: target.service
|
|
309
|
-
},
|
|
310
|
-
next: provision
|
|
311
|
-
};
|
|
312
|
-
case "bucket":
|
|
313
|
-
if (target.name.length < 1 || target.name.length > 255) throw new Error("A bucket name must be 1-255 characters.");
|
|
314
|
-
if (target.name === "__proto__") throw new Error("\"__proto__\" cannot be used as a bucket name.");
|
|
315
|
-
return {
|
|
316
|
-
edit: {
|
|
317
|
-
kind: "bucket",
|
|
318
|
-
name: target.name,
|
|
319
|
-
access: target.access ?? "private"
|
|
320
|
-
},
|
|
321
|
-
next: provision
|
|
322
|
-
};
|
|
323
|
-
case "function": {
|
|
324
|
-
if (!FUNCTION_SLUG_PATTERN.test(target.slug)) {
|
|
325
|
-
const suggestion = deriveFunctionSlug(target.slug);
|
|
326
|
-
throw new Error(`"${target.slug}" is not a valid function slug: use 1-20 lowercase letters and digits, no hyphens.` + (suggestion ? ` Try "${suggestion}".` : ""));
|
|
327
|
-
}
|
|
328
|
-
if (target.source !== void 0 && !SCRIPT_EXTENSION.test(target.source)) throw new Error(`--source must be a .ts or .js file, got "${target.source}". Declare a directory source in neon.ts by hand.`);
|
|
329
|
-
const source = asRelativeModulePath(target.source ?? `functions/${target.slug}.${language}`);
|
|
330
|
-
return {
|
|
331
|
-
edit: {
|
|
332
|
-
kind: "function",
|
|
333
|
-
slug: target.slug,
|
|
334
|
-
name: target.name ?? target.slug,
|
|
335
|
-
source
|
|
336
|
-
},
|
|
337
|
-
sourceFile: resolve(dirname(configPath), source),
|
|
338
|
-
next: explicitConfig ? `Next: \`${cli} config apply${flag}\` to deploy.` : `Next: \`${cli} dev\` to run it locally, \`${cli} config apply\` to deploy.`
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
/**
|
|
344
|
-
* Declare a service, feature, function, or bucket in the project's `neon.ts` with a text edit that
|
|
345
|
-
* leaves the rest of the file alone, creating the file (and installing the config packages)
|
|
346
|
-
* when the project has none. Local only, like `config init`.
|
|
347
|
-
*/
|
|
348
|
-
const addCmd = async (props) => {
|
|
349
|
-
const cwd = props.cwd ?? process.cwd();
|
|
350
|
-
const location = {
|
|
351
|
-
cwd,
|
|
352
|
-
...props.contextFile ? { contextFile: props.contextFile } : {}
|
|
353
|
-
};
|
|
354
|
-
let existing;
|
|
355
|
-
if (props.config) {
|
|
356
|
-
existing = resolve(cwd, props.config);
|
|
357
|
-
if (!existsSync(existing)) throw new Error(`--config ${props.config} does not exist.`);
|
|
358
|
-
} else existing = projectConfigPath(location);
|
|
359
|
-
const configPath = existing ?? join(projectDir(location), "neon.ts");
|
|
360
|
-
const shown = (path) => relative(cwd, path) || path;
|
|
361
|
-
const plan = planAdd(props.target, configPath, props.config);
|
|
362
|
-
const { source, changes } = editNeonConfig(existing ? readFileSync(existing, "utf8") : renderNeonConfig([]), plan.edit);
|
|
363
|
-
if (changes.length === 0) {
|
|
364
|
-
if (props.target.kind === "service" || props.target.kind === "realtime") {
|
|
365
|
-
log.info("%s already has the requested configuration; nothing to change.", shown(configPath));
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
throw new Error(`${shown(configPath)} already declares ${props.target.kind} "${props.target.kind === "function" ? props.target.slug : props.target.name}". Edit it there, or pick another name.`);
|
|
369
|
-
}
|
|
370
|
-
if (plan.sourceFile) {
|
|
371
|
-
if (existsSync(plan.sourceFile)) log.info("Found an existing %s — leaving it untouched.", shown(plan.sourceFile));
|
|
372
|
-
else {
|
|
373
|
-
mkdirSync(dirname(plan.sourceFile), { recursive: true });
|
|
374
|
-
const slug = props.target.kind === "function" ? props.target.slug : "";
|
|
375
|
-
writeFileSync(plan.sourceFile, renderFunctionSource(slug, TYPESCRIPT_EXTENSION.test(plan.sourceFile) ? "ts" : "js"));
|
|
376
|
-
log.info("Created %s.", shown(plan.sourceFile));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
writeFileSync(configPath, source);
|
|
380
|
-
log.info("%s %s: %s.", existing ? "Updated" : "Created", shown(configPath), changes.join(", "));
|
|
381
|
-
if (!existing) await ensureConfigPackages(dirname(configPath), props);
|
|
382
|
-
log.info("%s", plan.next);
|
|
383
|
-
};
|
|
384
|
-
/**
|
|
385
|
-
* Options every `config add` sub-command takes. `--project-id` and `--branch` are inherited
|
|
386
|
-
* from `config` but mean nothing to a command that never leaves the working tree, so they are
|
|
387
|
-
* hidden from its help.
|
|
388
|
-
*/
|
|
389
|
-
const addSharedOptions = (yargs) => yargs.strict().options({
|
|
390
|
-
"project-id": { hidden: true },
|
|
391
|
-
branch: { hidden: true },
|
|
392
|
-
config: {
|
|
393
|
-
describe: "Path to the neon.ts to edit (defaults to the neon.ts next to .neon, or in cwd without one; created there when missing)",
|
|
394
|
-
type: "string"
|
|
395
|
-
},
|
|
396
|
-
install: {
|
|
397
|
-
describe: "Install @neon/config and @neon/env when creating neon.ts. On by default; use --no-install to just print the command.",
|
|
398
|
-
type: "boolean",
|
|
399
|
-
default: true
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
/** The `config add` props for a sub-command's parsed flags. */
|
|
403
|
-
const addProps = (args, target) => ({
|
|
404
|
-
target,
|
|
405
|
-
install: args.install,
|
|
406
|
-
...args.config !== void 0 ? { config: args.config } : {},
|
|
407
|
-
...args.contextFile !== void 0 ? { contextFile: args.contextFile } : {}
|
|
408
|
-
});
|
|
409
|
-
const command = "config";
|
|
410
|
-
const describe = "Manage a branch with a neon.ts policy";
|
|
411
|
-
const builder = (argv) => argv.usage("$0 config <sub-command> [options]").options({
|
|
412
|
-
"project-id": {
|
|
413
|
-
describe: "Project ID",
|
|
414
|
-
type: "string"
|
|
415
|
-
},
|
|
416
|
-
branch: {
|
|
417
|
-
describe: "Branch ID or name",
|
|
418
|
-
type: "string"
|
|
419
|
-
}
|
|
420
|
-
}).middleware(fillSingleProject).command("status", "Show the branch's live Neon state", (yargs) => yargs.options({
|
|
421
|
-
"config-json": {
|
|
422
|
-
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.",
|
|
423
|
-
type: "boolean",
|
|
424
|
-
default: false
|
|
425
|
-
},
|
|
426
|
-
"current-branch": {
|
|
427
|
-
describe: "Print only the linked branch name from the local .neon file (no network). Exits non-zero when no branch is pinned.",
|
|
428
|
-
type: "boolean",
|
|
429
|
-
default: false
|
|
430
|
-
}
|
|
431
|
-
}), (args) => status(args)).command("plan", "Show what `config apply` would change (dry run)", (yargs) => yargs.options({
|
|
432
|
-
config: {
|
|
433
|
-
describe: "Path to a neon.ts policy (defaults to the neon.ts next to .neon, or in cwd without one)",
|
|
434
|
-
type: "string"
|
|
435
|
-
},
|
|
436
|
-
...envFlag
|
|
437
|
-
}), (args) => planCmd(args)).command("apply", "Apply a neon.ts policy to the branch", (yargs) => yargs.options({
|
|
438
|
-
config: {
|
|
439
|
-
describe: "Path to a neon.ts policy (defaults to the neon.ts next to .neon, or in cwd without one)",
|
|
440
|
-
type: "string"
|
|
441
|
-
},
|
|
442
|
-
...envFlag,
|
|
443
|
-
...applyFlags,
|
|
444
|
-
...envPullFlag
|
|
445
|
-
}), (args) => applyCmd(args)).command("add", "Add a service, function, or bucket to neon.ts (creates it if missing)", (yargs) => yargs.command("auth", "Enable Neon Auth (auth: true)", addSharedOptions, (args) => addCmd(addProps(args, {
|
|
446
|
-
kind: "service",
|
|
447
|
-
service: "auth"
|
|
448
|
-
}))).command("data-api", "Enable the Data API (dataApi: true) and Neon Auth, which the default provider requires", addSharedOptions, (args) => addCmd(addProps(args, {
|
|
449
|
-
kind: "service",
|
|
450
|
-
service: "data-api"
|
|
451
|
-
}))).command("ai-gateway", "Enable the AI Gateway (aiGateway: true)", addSharedOptions, (args) => addCmd(addProps(args, {
|
|
452
|
-
kind: "service",
|
|
453
|
-
service: "ai-gateway"
|
|
454
|
-
}))).command("realtime", "Enable Realtime (preview.realtime: true)", (yargs) => addSharedOptions(yargs).option("allowed-origin", {
|
|
455
|
-
describe: "Allowed browser origin (repeatable). \"*\" allows every origin",
|
|
456
|
-
type: "array",
|
|
457
|
-
string: true
|
|
458
|
-
}), (args) => addCmd(addProps(args, {
|
|
459
|
-
kind: "realtime",
|
|
460
|
-
...args.allowedOrigin !== void 0 ? { allowedOrigins: args.allowedOrigin } : {}
|
|
461
|
-
}))).command("function <slug>", "Declare a Neon Function and create its handler file", (yargs) => addSharedOptions(yargs).example("$0 config add function sendemail", "Creates functions/sendemail.ts and declares it").example("$0 config add function sendemail --source src/send.ts", "Declares an existing handler").positional("slug", {
|
|
462
|
-
describe: "Function slug: 1-20 lowercase letters and digits",
|
|
463
|
-
type: "string",
|
|
464
|
-
demandOption: true
|
|
465
|
-
}).options({
|
|
466
|
-
name: {
|
|
467
|
-
describe: "Display name. Defaults to the slug",
|
|
468
|
-
type: "string"
|
|
469
|
-
},
|
|
470
|
-
source: {
|
|
471
|
-
describe: "Handler file, relative to neon.ts. Defaults to functions/<slug>.ts. Created when missing, left alone when it exists",
|
|
472
|
-
type: "string"
|
|
473
|
-
}
|
|
474
|
-
}), (args) => addCmd(addProps(args, {
|
|
475
|
-
kind: "function",
|
|
476
|
-
slug: args.slug,
|
|
477
|
-
...args.name !== void 0 ? { name: args.name } : {},
|
|
478
|
-
...args.source !== void 0 ? { source: args.source } : {}
|
|
479
|
-
}))).command("bucket <name>", "Declare an Object Storage bucket", (yargs) => addSharedOptions(yargs).example("$0 config add bucket assets --access public_read", "Declares a bucket that allows anonymous reads").positional("name", {
|
|
480
|
-
describe: "Bucket name",
|
|
481
|
-
type: "string",
|
|
482
|
-
demandOption: true
|
|
483
|
-
}).options({ access: {
|
|
484
|
-
describe: "Anonymous access: private requires credentials, public_read allows anonymous reads",
|
|
485
|
-
type: "string",
|
|
486
|
-
choices: ["private", "public_read"],
|
|
487
|
-
default: "private"
|
|
488
|
-
} }), (args) => addCmd(addProps(args, {
|
|
489
|
-
kind: "bucket",
|
|
490
|
-
name: args.name,
|
|
491
|
-
access: args.access
|
|
492
|
-
})))).command("init", "Scaffold a neon.ts policy and install the Neon config packages", (yargs) => yargs.strict().options({
|
|
493
|
-
install: {
|
|
494
|
-
describe: "Install @neon/config and @neon/env if they're missing. On by default; use --no-install to just print the command.",
|
|
495
|
-
type: "boolean",
|
|
496
|
-
default: true
|
|
497
|
-
},
|
|
498
|
-
services: servicesOption({
|
|
499
|
-
key: "services",
|
|
500
|
-
allowed: NEON_SERVICES,
|
|
501
|
-
describe: "Services the scaffolded neon.ts declares",
|
|
502
|
-
also: "postgres alone writes the starter policy; every branch has Postgres. Omitted: pick interactively on a terminal, starter policy in CI or without a TTY."
|
|
503
|
-
}),
|
|
504
|
-
"from-branch": {
|
|
505
|
-
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.",
|
|
506
|
-
type: "boolean",
|
|
507
|
-
conflicts: "services"
|
|
508
|
-
}
|
|
509
|
-
}), (args) => initCmd({
|
|
510
|
-
...args,
|
|
511
|
-
services: servicesFlagValue(args.services)
|
|
512
|
-
}));
|
|
513
|
-
const handler = (args) => {
|
|
514
|
-
return args;
|
|
515
|
-
};
|
|
516
|
-
/**
|
|
517
|
-
* A branch's live state, plus that state projected into the `neon.ts`-shaped
|
|
518
|
-
* {@link NeonConfigView}. Shared by `config status` and `config init --from-branch` so both
|
|
519
|
-
* read the branch through one path: what `status --config-json` prints is exactly what
|
|
520
|
-
* `init --from-branch` writes.
|
|
521
|
-
*
|
|
522
|
-
* The pulled `config` carries the branch's tuning inside a closure that JSON can't render, so
|
|
523
|
-
* it is resolved against the live branch target first.
|
|
524
|
-
*/
|
|
525
|
-
const liveConfigView = async (opts) => {
|
|
526
|
-
const live = await inspect({
|
|
527
|
-
projectId: opts.projectId,
|
|
528
|
-
branchId: opts.branchId,
|
|
529
|
-
...opts.apiKey ? { apiKey: opts.apiKey } : {},
|
|
530
|
-
...opts.apiHost ? { apiHost: opts.apiHost } : {},
|
|
531
|
-
...opts.runtimeApi ? { api: opts.runtimeApi } : {}
|
|
532
|
-
});
|
|
533
|
-
const resolved = resolveConfig(live.config, {
|
|
534
|
-
name: live.branch.name,
|
|
535
|
-
id: live.branch.id,
|
|
536
|
-
exists: true,
|
|
537
|
-
isDefault: live.branch.isDefault,
|
|
538
|
-
isProtected: live.branch.protected,
|
|
539
|
-
...live.branch.parent ? { parentId: live.branch.parent } : {},
|
|
540
|
-
...live.branch.expiresAt ? { expiresAt: live.branch.expiresAt } : {}
|
|
541
|
-
});
|
|
542
|
-
return {
|
|
543
|
-
live,
|
|
544
|
-
view: toNeonConfigView(resolved, live.preview)
|
|
545
|
-
};
|
|
546
|
-
};
|
|
547
|
-
const loadEnvFileIfGiven = (env) => {
|
|
548
|
-
if (!env) return;
|
|
549
|
-
const applied = loadEnvFileIntoProcess(env);
|
|
550
|
-
log.debug("Loaded %d var(s) from %s into the environment: %s", applied.length, env, applied.join(", "));
|
|
551
|
-
};
|
|
552
|
-
const loadConfig = async (props) => {
|
|
553
|
-
loadEnvFileIfGiven(props.env);
|
|
554
|
-
const { config } = await loadProjectConfig({
|
|
555
|
-
contextFile: props.contextFile,
|
|
556
|
-
...props.config ? { path: props.config } : {}
|
|
557
|
-
});
|
|
558
|
-
return config;
|
|
559
|
-
};
|
|
560
|
-
const status = async (props) => {
|
|
561
|
-
if (props.currentBranch) {
|
|
562
|
-
const branch = contextBranch(readContextFile(props.contextFile));
|
|
563
|
-
if (branch) process.stdout.write(`${branch}\n`);
|
|
564
|
-
else {
|
|
565
|
-
log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
566
|
-
process.exitCode = 1;
|
|
567
|
-
}
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
const branch = await resolveBranchRef(props);
|
|
571
|
-
if (!props.configJson) announceTargetBranch(props, branch, "Inspecting branch");
|
|
572
|
-
const { live, view: configView } = await liveConfigView({
|
|
573
|
-
projectId: props.projectId,
|
|
574
|
-
branchId: branch.branchId,
|
|
575
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
576
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
577
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
|
|
578
|
-
});
|
|
579
|
-
if (props.configJson) {
|
|
580
|
-
process.stdout.write(`${JSON.stringify(configView, null, 2)}\n`);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (props.output !== "table") {
|
|
584
|
-
writer(props).end({
|
|
585
|
-
project: live.project,
|
|
586
|
-
branch: live.branch,
|
|
587
|
-
config: configView
|
|
588
|
-
}, { fields: INSPECT_FIELDS });
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
writeStatusTables(props, live, configView);
|
|
592
|
-
};
|
|
593
|
-
const STATUS_FIELDS = [
|
|
594
|
-
"project",
|
|
595
|
-
"region",
|
|
596
|
-
"postgres_version",
|
|
597
|
-
"organization",
|
|
598
|
-
"branch",
|
|
599
|
-
"parent",
|
|
600
|
-
"default",
|
|
601
|
-
"protected",
|
|
602
|
-
"expires_at",
|
|
603
|
-
"auth",
|
|
604
|
-
"data_api",
|
|
605
|
-
"compute",
|
|
606
|
-
"scale_to_zero",
|
|
607
|
-
"ttl"
|
|
608
|
-
];
|
|
609
|
-
const formatComputeUnits = (settings) => {
|
|
610
|
-
const min = settings?.autoscalingLimitMinCu;
|
|
611
|
-
const max = settings?.autoscalingLimitMaxCu;
|
|
612
|
-
if (min === void 0 && max === void 0) return void 0;
|
|
613
|
-
if (min === void 0) return `max ${max} CU`;
|
|
614
|
-
if (max === void 0) return `min ${min} CU`;
|
|
615
|
-
return min === max ? `${min} CU` : `${min}-${max} CU`;
|
|
616
|
-
};
|
|
617
|
-
/** Human `status`: one summary block, then a table per reported resource kind. */
|
|
618
|
-
const writeStatusTables = (props, live, view) => {
|
|
619
|
-
const compute = view.branch?.postgres?.computeSettings;
|
|
620
|
-
const suspend = compute?.suspendTimeout;
|
|
621
|
-
const out = writer(props);
|
|
622
|
-
out.write({
|
|
623
|
-
project: `${live.project.name} (${live.project.id})`,
|
|
624
|
-
region: live.project.region,
|
|
625
|
-
postgres_version: live.project.pgVersion,
|
|
626
|
-
organization: live.project.orgId,
|
|
627
|
-
branch: `${live.branch.name} (${live.branch.id})`,
|
|
628
|
-
parent: live.branch.parent,
|
|
629
|
-
default: live.branch.isDefault ? "yes" : "no",
|
|
630
|
-
protected: live.branch.protected ? "yes" : "no",
|
|
631
|
-
expires_at: live.branch.expiresAt,
|
|
632
|
-
auth: view.auth ? "enabled" : void 0,
|
|
633
|
-
data_api: view.dataApi ? "enabled" : void 0,
|
|
634
|
-
compute: formatComputeUnits(compute),
|
|
635
|
-
scale_to_zero: suspend === void 0 ? void 0 : suspend === false ? "off" : String(suspend),
|
|
636
|
-
ttl: view.branch?.ttl
|
|
637
|
-
}, {
|
|
638
|
-
fields: STATUS_FIELDS,
|
|
639
|
-
humanTitle: "Status"
|
|
640
|
-
});
|
|
641
|
-
const functions = Object.entries(view.preview?.functions ?? {}).map(([slug, fn]) => ({
|
|
642
|
-
slug,
|
|
643
|
-
name: fn.name
|
|
644
|
-
}));
|
|
645
|
-
if (functions.length > 0) out.write(functions, {
|
|
646
|
-
fields: ["slug", "name"],
|
|
647
|
-
humanTitle: "Functions"
|
|
648
|
-
});
|
|
649
|
-
const buckets = Object.entries(view.preview?.buckets ?? {}).map(([name, bucket]) => ({
|
|
650
|
-
name,
|
|
651
|
-
access: bucket.access
|
|
652
|
-
}));
|
|
653
|
-
if (buckets.length > 0) out.write(buckets, {
|
|
654
|
-
fields: ["name", "access"],
|
|
655
|
-
humanTitle: "Buckets"
|
|
656
|
-
});
|
|
657
|
-
const credentials = (view.preview?.credentials ?? []).map((credential) => ({
|
|
658
|
-
id: credential.id,
|
|
659
|
-
name: credential.name,
|
|
660
|
-
scopes: credential.scopes.join(", "),
|
|
661
|
-
last_used_at: credential.lastUsedAt
|
|
662
|
-
}));
|
|
663
|
-
if (credentials.length > 0) out.write(credentials, {
|
|
664
|
-
fields: [
|
|
665
|
-
"id",
|
|
666
|
-
"name",
|
|
667
|
-
"scopes",
|
|
668
|
-
"last_used_at"
|
|
669
|
-
],
|
|
670
|
-
humanTitle: "Credentials"
|
|
671
|
-
});
|
|
672
|
-
out.end();
|
|
673
|
-
};
|
|
674
|
-
const planCmd = async (props) => {
|
|
675
|
-
const config = await loadConfig(props);
|
|
676
|
-
const branch = await resolveBranchRef(props);
|
|
677
|
-
announceTargetBranch(props, branch, "Planning against branch");
|
|
678
|
-
const branchId = branch.branchId;
|
|
679
|
-
const result = await plan(config, {
|
|
680
|
-
projectId: props.projectId,
|
|
681
|
-
branchId,
|
|
682
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
683
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
684
|
-
...props.runtimeApi ? { api: props.runtimeApi } : {}
|
|
685
|
-
});
|
|
686
|
-
const services = utilizedServices(config);
|
|
687
|
-
reportPushResult(props, result, "plan", services);
|
|
688
|
-
if (result.conflicts.some((conflict) => conflict.field === "customDomain" && !reasonAllowsUpdateExisting(conflict.reason))) process.exitCode = 1;
|
|
689
|
-
if (services.includes("AI Gateway")) await warnAiGateway({
|
|
690
|
-
apiClient: props.apiClient,
|
|
691
|
-
projectId: props.projectId,
|
|
692
|
-
branchId
|
|
693
|
-
});
|
|
694
|
-
};
|
|
695
|
-
const warnDeprecatedPreview = (config, target) => {
|
|
696
|
-
const message = previewGaWarningForConfig(config, target);
|
|
697
|
-
if (message) log.warning("%s", message);
|
|
698
|
-
};
|
|
699
|
-
const warningTargetFromResolved = (branch) => ({
|
|
700
|
-
name: branch.branchName,
|
|
701
|
-
id: branch.branchId,
|
|
702
|
-
exists: true,
|
|
703
|
-
...branch.isDefault !== void 0 ? { isDefault: branch.isDefault } : {},
|
|
704
|
-
...branch.isProtected !== void 0 ? { isProtected: branch.isProtected } : {},
|
|
705
|
-
...branch.parentId ? { parentId: branch.parentId } : {},
|
|
706
|
-
...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
|
|
707
|
-
});
|
|
708
|
-
const warningTargetForExistingBranch = async (props) => {
|
|
709
|
-
if (props.runtimeApi) {
|
|
710
|
-
const branch = (await props.runtimeApi.listBranches(props.projectId)).find((listed) => listed.id === props.branchId);
|
|
711
|
-
if (branch) return {
|
|
712
|
-
name: branch.name,
|
|
713
|
-
id: branch.id,
|
|
714
|
-
exists: true,
|
|
715
|
-
isDefault: branch.isDefault,
|
|
716
|
-
isProtected: branch.protected,
|
|
717
|
-
...branch.parentId ? { parentId: branch.parentId } : {},
|
|
718
|
-
...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
if (props.apiKey) {
|
|
722
|
-
const { data } = await getApiClient({
|
|
723
|
-
apiKey: props.apiKey,
|
|
724
|
-
...props.apiHost ? { apiHost: props.apiHost } : {}
|
|
725
|
-
}).listProjectBranches({ projectId: props.projectId });
|
|
726
|
-
const found = data.branches.find((listed) => listed.id === props.branchId);
|
|
727
|
-
if (found) return {
|
|
728
|
-
name: found.name ?? props.branchName ?? props.branchId,
|
|
729
|
-
id: found.id,
|
|
730
|
-
exists: true,
|
|
731
|
-
isDefault: found.default === true,
|
|
732
|
-
isProtected: found.protected === true,
|
|
733
|
-
...found.parent_id ? { parentId: found.parent_id } : {},
|
|
734
|
-
...found.expires_at ? { expiresAt: found.expires_at } : {}
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
return {
|
|
738
|
-
name: props.branchName ?? props.branchId,
|
|
739
|
-
id: props.branchId,
|
|
740
|
-
exists: true,
|
|
741
|
-
isDefault: false
|
|
742
|
-
};
|
|
743
|
-
};
|
|
744
|
-
const warnDeprecatedPreviewOnCreate = async (config, props) => {
|
|
745
|
-
try {
|
|
746
|
-
const listed = await warningTargetForExistingBranch(props);
|
|
747
|
-
warnDeprecatedPreview(config, {
|
|
748
|
-
...listed,
|
|
749
|
-
exists: false
|
|
750
|
-
});
|
|
751
|
-
} catch {
|
|
752
|
-
warnDeprecatedPreview(config, {
|
|
753
|
-
name: props.branchName,
|
|
754
|
-
id: props.branchId,
|
|
755
|
-
exists: false,
|
|
756
|
-
isDefault: false
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
const applyCmd = async (props) => {
|
|
761
|
-
const config = await loadConfig(props);
|
|
762
|
-
const branch = await resolveBranchRef(props);
|
|
763
|
-
announceTargetBranch(props, branch, "Applying to branch");
|
|
764
|
-
warnDeprecatedPreview(config, warningTargetFromResolved(branch));
|
|
765
|
-
const branchId = branch.branchId;
|
|
766
|
-
if (utilizedServices(config).includes("AI Gateway")) await assertAiGatewayProvisionable({
|
|
767
|
-
apiClient: props.apiClient,
|
|
768
|
-
projectId: props.projectId
|
|
769
|
-
});
|
|
770
|
-
const cwd = process.cwd();
|
|
771
|
-
const git = readGitContext(cwd);
|
|
772
|
-
const event = { type: "neon-deploy" };
|
|
773
|
-
const hooks = config.experimental?.hooks;
|
|
774
|
-
const hookBranch = {
|
|
775
|
-
projectId: props.projectId,
|
|
776
|
-
id: branch.branchId,
|
|
777
|
-
name: branch.branchName,
|
|
778
|
-
created: false,
|
|
779
|
-
isDefault: branch.isDefault ?? false,
|
|
780
|
-
isProtected: branch.isProtected ?? false,
|
|
781
|
-
...branch.parentId ? { parentId: branch.parentId } : {},
|
|
782
|
-
...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
|
|
783
|
-
};
|
|
784
|
-
let result;
|
|
785
|
-
let declined = false;
|
|
786
|
-
try {
|
|
787
|
-
result = await apply(config, {
|
|
788
|
-
projectId: props.projectId,
|
|
789
|
-
branchId,
|
|
790
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
791
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
792
|
-
...props.runtimeApi ? { api: props.runtimeApi } : {},
|
|
793
|
-
...props.updateExisting || props.yes ? { updateExisting: true } : {},
|
|
794
|
-
...props.allowProtected || props.yes ? { allowProtectedBranch: true } : {},
|
|
795
|
-
confirm: async (context) => {
|
|
796
|
-
if (context.overrides.length > 0) reportConflicts(props, context.overrides);
|
|
797
|
-
if (!canPromptForApply(props)) return false;
|
|
798
|
-
const proceed = await askToApply(context);
|
|
799
|
-
declined = !proceed;
|
|
800
|
-
return proceed;
|
|
801
|
-
},
|
|
802
|
-
beforeMutations: () => runDeployBeforeHook({
|
|
803
|
-
hooks,
|
|
804
|
-
branch: hookBranch,
|
|
805
|
-
git,
|
|
806
|
-
event,
|
|
807
|
-
cwd
|
|
808
|
-
}),
|
|
809
|
-
bundleFunction: neonctlBundler
|
|
810
|
-
});
|
|
811
|
-
} catch (err) {
|
|
812
|
-
if (err instanceof PushAbortedError) throw new Error(declined ? `Aborted: nothing was applied to branch "${err.branchName}".` : abortedApplyMessage(err));
|
|
813
|
-
if (err instanceof PushConflictError) {
|
|
814
|
-
reportConflicts(props, err.conflicts);
|
|
815
|
-
throw new Error("The policy conflicts with remote state. --update-existing will not resolve every conflict shown above.");
|
|
816
|
-
}
|
|
817
|
-
throw err;
|
|
818
|
-
}
|
|
819
|
-
reportPushResult(props, result, "apply", utilizedServices(config));
|
|
820
|
-
await autoPullEnvAfterPin({
|
|
821
|
-
...props,
|
|
822
|
-
envPull: props.envPull !== false
|
|
823
|
-
}, branch);
|
|
824
|
-
if (hooks?.deploy?.after) {
|
|
825
|
-
const env = await resolveHookEnv(config, {
|
|
826
|
-
projectId: props.projectId,
|
|
827
|
-
branch: branchId,
|
|
828
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
829
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
830
|
-
...props.runtimeApi ? { api: props.runtimeApi } : {}
|
|
831
|
-
});
|
|
832
|
-
if (env) await runDeployAfterHook({
|
|
833
|
-
hooks,
|
|
834
|
-
branch: hookBranch,
|
|
835
|
-
env,
|
|
836
|
-
result,
|
|
837
|
-
git,
|
|
838
|
-
event,
|
|
839
|
-
cwd
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
};
|
|
843
|
-
/**
|
|
844
|
-
* Human-readable list of the services a `neon.ts` policy utilizes on the branch, shown under
|
|
845
|
-
* the plan/apply table. Postgres is always present (every branch has it); the rest are listed
|
|
846
|
-
* only when the policy declares them. This deliberately surfaces services that produce **no**
|
|
847
|
-
* plan step — notably the AI Gateway, which is always available and only needs a scoped branch
|
|
848
|
-
* credential (not a provisioning step) — so adding `aiGateway` to a neon.ts isn't
|
|
849
|
-
* mistaken for being silently dropped. Service enablement is static top-level config (it never
|
|
850
|
-
* lives in the per-branch closure), so reading it straight off `config` is accurate.
|
|
851
|
-
*/
|
|
852
|
-
const utilizedServices = (config) => [NEON_SERVICE_LABELS.postgres, ...declaredNeonServices(config).map((service) => NEON_SERVICE_LABELS[service])];
|
|
853
|
-
/**
|
|
854
|
-
* Render a {@link PushResult}. JSON/YAML output emits the raw result (plus a `services`
|
|
855
|
-
* summary) verbatim so it can be piped; the human-readable path renders the actual changes
|
|
856
|
-
* (dropping noops) and any blocking conflicts as a `git diff`-style report, or a "nothing to
|
|
857
|
-
* do" line when both are empty — and always closes with the list of services the policy
|
|
858
|
-
* utilizes so a service that produces no plan step (Postgres, or the credential-gated AI
|
|
859
|
-
* Gateway) isn't mistaken for being missing from the plan above.
|
|
860
|
-
*
|
|
861
|
-
* The diff is asymmetric on purpose (see the CLI's `neon diff`): **service** changes are
|
|
862
|
-
* additions with no "before", so they list as `+`/`~` lines; **branch setting** changes have
|
|
863
|
-
* a natural before→after, so conflicts render as a sorted `current → desired` diff. Planned
|
|
864
|
-
* branch updates (under `--update-existing`) carry only the new value, so they render
|
|
865
|
-
* desired-only for now (the previous value isn't threaded through the runtime yet).
|
|
866
|
-
*/
|
|
867
|
-
const reportPushResult = (props, result, mode, services) => {
|
|
868
|
-
if (props.output === "json" || props.output === "yaml") {
|
|
869
|
-
writer(props).end({
|
|
870
|
-
...result,
|
|
871
|
-
services
|
|
872
|
-
}, { fields: [] });
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
const appliedChanges = result.applied.filter((change) => change.action !== "noop");
|
|
876
|
-
const functionUrlBySlug = /* @__PURE__ */ new Map();
|
|
877
|
-
for (const change of appliedChanges) {
|
|
878
|
-
const slug = change.details?.slug;
|
|
879
|
-
const invocationUrl = change.details?.invocationUrl;
|
|
880
|
-
if (typeof slug === "string" && typeof invocationUrl === "string") functionUrlBySlug.set(slug, invocationUrl);
|
|
881
|
-
}
|
|
882
|
-
const color = props.color !== false;
|
|
883
|
-
const out = writer(props);
|
|
884
|
-
const noChanges = appliedChanges.length === 0 && result.conflicts.length === 0;
|
|
885
|
-
const appliedText = renderAppliedChanges(appliedChanges, mode === "plan" ? "Planned changes" : "Applied changes", { color });
|
|
886
|
-
if (appliedText) out.text(`${appliedText}\n`);
|
|
887
|
-
if (result.conflicts.length > 0) {
|
|
888
|
-
const conflictText = renderBranchSettingConflicts(result.conflicts, { color });
|
|
889
|
-
if (conflictText) out.text(`\n${conflictText}\n`);
|
|
890
|
-
for (const conflict of result.conflicts) if (!reasonAllowsUpdateExisting(conflict.reason)) out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
|
|
891
|
-
}
|
|
892
|
-
const followup = renderCustomDomainFollowup(result, { color });
|
|
893
|
-
if (followup) out.text(`\n${followup}\n`);
|
|
894
|
-
if (functionUrlBySlug.size > 0) {
|
|
895
|
-
const heading = mode === "plan" ? "Function URLs (after apply)" : "Function URLs";
|
|
896
|
-
out.text(`\n${isCi() ? heading : chalk.bold(heading)}\n`);
|
|
897
|
-
for (const [slug, invocationUrl] of functionUrlBySlug) out.text(` • ${slug}: ${invocationUrl}\n`);
|
|
898
|
-
}
|
|
899
|
-
if (noChanges) log.info(`No changes — branch ${result.branchName} already matches the policy.`);
|
|
900
|
-
out.text(`\nUtilized services: ${services.join(", ")}\n`);
|
|
901
|
-
};
|
|
902
|
-
/** A prompt needs both ends of a terminal, and would corrupt `-o json|yaml` on stdout. */
|
|
903
|
-
const canPromptForApply = (props) => isInteractiveTerminal() && props.output !== "json" && props.output !== "yaml";
|
|
904
|
-
const askToApply = async (context) => {
|
|
905
|
-
const branch = JSON.stringify(context.branchName);
|
|
906
|
-
const message = context.protectedBranch && context.overrideUpdates ? `Branch ${branch} is protected, and this overrides the settings shown above. Apply anyway?` : context.protectedBranch ? `Branch ${branch} is protected. Apply to it anyway?` : `Override the settings shown above on branch ${branch}?`;
|
|
907
|
-
const { default: prompts } = await import("prompts");
|
|
908
|
-
const { proceed } = await prompts({
|
|
909
|
-
type: "confirm",
|
|
910
|
-
name: "proceed",
|
|
911
|
-
message,
|
|
912
|
-
initial: false
|
|
913
|
-
});
|
|
914
|
-
return proceed === true;
|
|
915
|
-
};
|
|
916
|
-
/** Without a terminal there is no one to ask, so name the flags that answer the question. */
|
|
917
|
-
const abortedApplyMessage = (err) => {
|
|
918
|
-
const branch = JSON.stringify(err.branchName);
|
|
919
|
-
const isProtected = err.reasons.includes("protected-branch");
|
|
920
|
-
const overrides = err.reasons.includes("override-updates");
|
|
921
|
-
if (isProtected && overrides) return `Branch ${branch} is protected and its settings differ from the policy. Re-run with -y (or --allow-protected --update-existing) to apply the changes shown above.`;
|
|
922
|
-
if (isProtected) return `Branch ${branch} is protected. Re-run with --allow-protected (or -y) to apply to it.`;
|
|
923
|
-
return "Branch settings conflict with the policy. Re-run with --update-existing (or -y) to apply the changes shown above.";
|
|
924
|
-
};
|
|
925
|
-
/**
|
|
926
|
-
* Render the branch-setting {@link ConflictReport}s a bare `apply` refused to override (drift
|
|
927
|
-
* without `--update-existing`) as the git-style before→after diff. JSON/YAML output emits the
|
|
928
|
-
* structured conflicts so it can be piped; the human path prints the sorted diff followed by
|
|
929
|
-
* any conflict whose fix is *not* `--update-existing` (e.g. an immutable "no endpoint" case),
|
|
930
|
-
* so nothing the library's error message carried is lost.
|
|
931
|
-
*/
|
|
932
|
-
const reportConflicts = (props, conflicts) => {
|
|
933
|
-
if (props.output === "json" || props.output === "yaml") {
|
|
934
|
-
writer(props).end({ conflicts }, { fields: [] });
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
const out = writer(props);
|
|
938
|
-
const text = renderBranchSettingConflicts([...conflicts], { color: props.color !== false });
|
|
939
|
-
if (text) out.text(`${text}\n`);
|
|
940
|
-
for (const conflict of conflicts) if (!reasonAllowsUpdateExisting(conflict.reason)) out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
|
|
941
|
-
};
|
|
942
|
-
/**
|
|
943
|
-
* Block provisioning the AI Gateway on a Free plan from the `checkout` policy paths, which
|
|
944
|
-
* carry raw credentials (`apiKey`/`apiHost`) rather than the CLI's api client. Builds a client
|
|
945
|
-
* from them and defers to {@link assertAiGatewayProvisionable}. Skipped when a `runtimeApi` is
|
|
946
|
-
* injected (tests) or no `apiKey` is available; the interactive commands always have a key.
|
|
947
|
-
*/
|
|
948
|
-
const assertAiGatewayProvisionableFromCreds = async (props) => {
|
|
949
|
-
if (props.runtimeApi || !props.apiKey) return;
|
|
950
|
-
if (!utilizedServices(props.config).includes("AI Gateway")) return;
|
|
951
|
-
const apiClient = getApiClient({
|
|
952
|
-
apiKey: props.apiKey,
|
|
953
|
-
...props.apiHost ? { apiHost: props.apiHost } : {}
|
|
954
|
-
});
|
|
955
|
-
await assertAiGatewayProvisionable({
|
|
956
|
-
apiClient,
|
|
957
|
-
projectId: props.projectId
|
|
958
|
-
});
|
|
959
|
-
};
|
|
960
|
-
/**
|
|
961
|
-
* Apply a `neon.ts` policy to a **freshly created** branch (used by `neonctl checkout`
|
|
962
|
-
* when it creates a branch). No-op when there is no `neon.ts` on the path from cwd up to
|
|
963
|
-
* the repo root — checkout still succeeds, it just has no policy to apply.
|
|
964
|
-
*
|
|
965
|
-
* The branch was just created by us, so we apply non-interactively (`updateExisting` /
|
|
966
|
-
* `allowProtectedBranch`) — there is no pre-existing state a user would be surprised to
|
|
967
|
-
* see overridden. Functions are bundled with neonctl's own esbuild helper.
|
|
968
|
-
*/
|
|
969
|
-
const applyPolicyOnCreate = async (props) => {
|
|
970
|
-
loadEnvFileIfGiven(props.env);
|
|
971
|
-
let config;
|
|
972
|
-
try {
|
|
973
|
-
({config} = await loadProjectConfig({
|
|
974
|
-
...props.cwd ? { cwd: props.cwd } : {},
|
|
975
|
-
...props.contextFile ? { contextFile: props.contextFile } : {}
|
|
976
|
-
}));
|
|
977
|
-
} catch (err) {
|
|
978
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
979
|
-
if (/Could not find a Neon config file/i.test(message)) return;
|
|
980
|
-
throw err;
|
|
981
|
-
}
|
|
982
|
-
try {
|
|
983
|
-
warnDeprecatedPreview(config, await warningTargetForExistingBranch({
|
|
984
|
-
projectId: props.projectId,
|
|
985
|
-
branchId: props.branchId,
|
|
986
|
-
...props.branchName ? { branchName: props.branchName } : {},
|
|
987
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
988
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
989
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
|
|
990
|
-
}));
|
|
991
|
-
} catch {
|
|
992
|
-
warnDeprecatedPreview(config);
|
|
993
|
-
}
|
|
994
|
-
await assertAiGatewayProvisionableFromCreds({
|
|
995
|
-
projectId: props.projectId,
|
|
996
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
997
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
998
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {},
|
|
999
|
-
config
|
|
1000
|
-
});
|
|
1001
|
-
log.info("Applying neon.ts policy to the new branch…");
|
|
1002
|
-
const result = await apply(config, {
|
|
1003
|
-
projectId: props.projectId,
|
|
1004
|
-
branchId: props.branchId,
|
|
1005
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
1006
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
1007
|
-
...props.runtimeApi ? { api: props.runtimeApi } : {},
|
|
1008
|
-
updateExisting: true,
|
|
1009
|
-
allowProtectedBranch: true,
|
|
1010
|
-
bundleFunction: neonctlBundler
|
|
1011
|
-
});
|
|
1012
|
-
logPolicyResult(result, { color: props.color !== false });
|
|
1013
|
-
};
|
|
1014
|
-
/**
|
|
1015
|
-
* Report what applying a `neon.ts` policy changed, using the same `field → value` diff
|
|
1016
|
-
* `deploy` prints (see {@link renderAppliedChanges}) rather than a bare list of change
|
|
1017
|
-
* identifiers — the identifier alone repeats the branch name once per change and never says
|
|
1018
|
-
* *what* was applied, which is the only interesting part on a freshly created branch.
|
|
1019
|
-
*/
|
|
1020
|
-
const logPolicyResult = (result, opts) => {
|
|
1021
|
-
const changes = result.applied.filter((c) => c.action !== "noop");
|
|
1022
|
-
if (changes.length === 0 && result.conflicts.length === 0) {
|
|
1023
|
-
log.info("neon.ts applied — no changes were needed.");
|
|
1024
|
-
const followup = renderCustomDomainFollowup(result, opts);
|
|
1025
|
-
if (followup) log.info("%s", followup);
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
if (changes.length > 0) log.info("%s", renderAppliedChanges(changes, `neon.ts applied — ${changes.length} change${changes.length === 1 ? "" : "s"}:`, opts));
|
|
1029
|
-
if (result.conflicts.length > 0) {
|
|
1030
|
-
const conflictText = renderBranchSettingConflicts(result.conflicts, opts);
|
|
1031
|
-
if (conflictText) log.info("%s", conflictText);
|
|
1032
|
-
for (const conflict of result.conflicts) if (!reasonAllowsUpdateExisting(conflict.reason)) log.info(" ! %s: %s", conflict.field, conflict.reason);
|
|
1033
|
-
}
|
|
1034
|
-
const followup = renderCustomDomainFollowup(result, opts);
|
|
1035
|
-
if (followup) log.info("%s", followup);
|
|
1036
|
-
};
|
|
1037
|
-
/**
|
|
1038
|
-
* Create a branch **from** the local `neon.ts` policy. Returns `null` when there is no
|
|
1039
|
-
* `neon.ts` on the path from cwd up to the repo root, so `neonctl checkout` can fall back to a
|
|
1040
|
-
* bare branch create.
|
|
1041
|
-
*
|
|
1042
|
-
* Unlike a bare create followed by {@link applyPolicyOnCreate}, this evaluates the policy for
|
|
1043
|
-
* the **new** branch (`exists: false`): the runtime branches from the policy's `parent` and
|
|
1044
|
-
* brings the branch up with its declared TTL / compute settings / services. That's what makes
|
|
1045
|
-
* a policy keyed on `!branch.exists` (the common "only configure new branches" shape) take
|
|
1046
|
-
* effect on the very first `checkout` — a bare create + `apply` always saw `exists: true` and
|
|
1047
|
-
* skipped that block.
|
|
1048
|
-
*
|
|
1049
|
-
* A branch that was created but whose policy failed to apply is reported through
|
|
1050
|
-
* `policyFailure` rather than thrown: the branch is real, so `checkout` still needs to pin it
|
|
1051
|
-
* (see the handler) instead of leaving it stranded behind an unchanged `.neon`.
|
|
1052
|
-
*/
|
|
1053
|
-
const createBranchFromPolicyOnCheckout = async (props) => {
|
|
1054
|
-
loadEnvFileIfGiven(props.env);
|
|
1055
|
-
let config;
|
|
1056
|
-
try {
|
|
1057
|
-
({config} = await loadProjectConfig({
|
|
1058
|
-
...props.cwd ? { cwd: props.cwd } : {},
|
|
1059
|
-
...props.contextFile ? { contextFile: props.contextFile } : {}
|
|
1060
|
-
}));
|
|
1061
|
-
} catch (err) {
|
|
1062
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1063
|
-
if (/Could not find a Neon config file/i.test(message)) return null;
|
|
1064
|
-
throw err;
|
|
1065
|
-
}
|
|
1066
|
-
await assertAiGatewayProvisionableFromCreds({
|
|
1067
|
-
projectId: props.projectId,
|
|
1068
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
1069
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
1070
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {},
|
|
1071
|
-
config
|
|
1072
|
-
});
|
|
1073
|
-
try {
|
|
1074
|
-
const { branchId, branchName, result } = await createBranch(config, {
|
|
1075
|
-
projectId: props.projectId,
|
|
1076
|
-
branchName: props.branchName,
|
|
1077
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
1078
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
1079
|
-
...props.runtimeApi ? { api: props.runtimeApi } : {},
|
|
1080
|
-
bundleFunction: neonctlBundler
|
|
1081
|
-
});
|
|
1082
|
-
log.info("Created branch %s (%s) from neon.ts policy.", branchName, branchId);
|
|
1083
|
-
logPolicyResult(result, { color: props.color !== false });
|
|
1084
|
-
await warnDeprecatedPreviewOnCreate(config, {
|
|
1085
|
-
projectId: props.projectId,
|
|
1086
|
-
branchId,
|
|
1087
|
-
branchName,
|
|
1088
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
1089
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
1090
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
|
|
1091
|
-
});
|
|
1092
|
-
return { branchId };
|
|
1093
|
-
} catch (err) {
|
|
1094
|
-
if (isPartialBranchCreateError(err)) {
|
|
1095
|
-
log.info("Created branch %s (%s) from neon.ts policy.", err.branchName, err.branchId);
|
|
1096
|
-
await warnDeprecatedPreviewOnCreate(config, {
|
|
1097
|
-
projectId: props.projectId,
|
|
1098
|
-
branchId: err.branchId,
|
|
1099
|
-
branchName: err.branchName,
|
|
1100
|
-
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
1101
|
-
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
1102
|
-
...props.runtimeApi ? { runtimeApi: props.runtimeApi } : {}
|
|
1103
|
-
});
|
|
1104
|
-
return {
|
|
1105
|
-
branchId: err.branchId,
|
|
1106
|
-
policyFailure: err.reason
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
throw err;
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
//#endregion
|
|
1113
|
-
export { ConfigInstallFailed, addCmd, applyCmd, applyFlags, applyPolicyOnCreate, builder, command, createBranchFromPolicyOnCheckout, describe, ensureConfigPackages, envFlag, envPullFlag, handler, initCmd, installCommandFrom, missingDependencies, planCmd, status, config_exports as t };
|
|
1
|
+
export { t as ConfigInstallFailed, n as addCmd, r as applyCmd, i as applyFlags, a as applyPolicyOnCreate, o as builder, s as command, l as createBranchFromPolicyOnCheckout, u as describe, d as ensureConfigPackages, f as envFlag, p as envPullFlag, m as handler, h as initCmd, g as installCommandFrom, _ as missingDependencies, v as planCmd, y as status } from "../_chunks/config-BH_qRHiG.js";
|