neon 6.0.0 → 6.2.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/dist/mcp/plan.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { tryResolveAddMcpAgentId } from "./agents.js";
2
2
  import { detectMcpAgents, mcpInstallableAgents } from "./targets.js";
3
3
  import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
4
- import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
5
4
  import { detectAgent } from "../init/detect_host.js";
5
+ import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
6
6
  import { pickMcpAuth, pickMcpProjectPin, pickMcpScope } from "./wizard.js";
7
7
  //#region src/mcp/plan.ts
8
8
  async function resolveMcpPlan(options) {
@@ -2,8 +2,8 @@ import { getAgentDisplayName, tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
3
  import { detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, pluginsInstallableAgents } from "./targets.js";
4
4
  import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
5
- import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
6
5
  import { detectAgent } from "../init/detect_host.js";
6
+ import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
7
7
  //#region src/plugins/plan.ts
8
8
  const assertPluginsCanRun = (options) => {
9
9
  if (options.yes || options.interactive || options.hasAgents) return;
@@ -2,8 +2,8 @@ import { tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
3
  import { detectSkillsAgents, skillsInstallableAgents } from "./targets.js";
4
4
  import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
5
- import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
6
5
  import { detectAgent } from "../init/detect_host.js";
6
+ import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
7
7
  import { invocationsForSelection, resolveSkillId, uniqueSkillEntries, yesInstallInvocations } from "./catalog.js";
8
8
  import { pickSkillsInteractively } from "./wizard.js";
9
9
  //#region src/skills/plan.ts
@@ -0,0 +1,29 @@
1
+ //#region src/utils/branch_name.ts
2
+ const MAX_LENGTH = 256;
3
+ const FALLBACK_NAME = "branch";
4
+ /** Reduce one path segment to `[a-z0-9-]`, collapsing other runs to `-` and trimming `-`. */
5
+ const sanitizeSegment = (segment) => segment.replace(/[^a-z0-9]+/gi, "-").replace(/^-+/, "").replace(/-+$/, "");
6
+ /**
7
+ * Convert a git branch name into a valid, stable Neon branch name — lowercased, each
8
+ * `/`-separated segment reduced to `[a-z0-9-]` (runs of other characters collapsed to a
9
+ * single `-`, leading/trailing `-` trimmed), `/` preserved as a segment separator so a
10
+ * hierarchical branch like `feature/billing-ui` round-trips unchanged, empty results fall
11
+ * back to `"branch"`, and the result is clamped to 256 characters (Neon's branch-name
12
+ * limit). This is the computation behind `GitContext.neonSafeBranchName` (from
13
+ * `@neon/config`) — call it directly only where you don't already have a `GitContext`
14
+ * (e.g. `neon git sync` deriving the default Neon branch name for the current git branch).
15
+ *
16
+ * @example neonSafeBranchName("feature/PROJ-123 Add Billing") // "feature/proj-123-add-billing"
17
+ * @example neonSafeBranchName("Hotfix!!") // "hotfix"
18
+ */
19
+ const neonSafeBranchName = (gitBranch) => {
20
+ const name = gitBranch.toLowerCase().split("/").map(sanitizeSegment).filter((segment) => segment.length > 0).join("/");
21
+ if (name.length === 0) return FALLBACK_NAME;
22
+ if (name.length > MAX_LENGTH) {
23
+ const clamped = name.slice(0, MAX_LENGTH).replace(/[-/]+$/, "");
24
+ return clamped.length === 0 ? FALLBACK_NAME : clamped;
25
+ }
26
+ return name;
27
+ };
28
+ //#endregion
29
+ export { neonSafeBranchName };
@@ -0,0 +1,220 @@
1
+ import { log } from "../log.js";
2
+ import { neonSafeBranchName } from "./branch_name.js";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, resolve } from "node:path";
5
+ import { execFileSync } from "node:child_process";
6
+ //#region src/utils/git.ts
7
+ /**
8
+ * Env var the installed `post-checkout` hook sets before invoking `neon git sync`, so the
9
+ * CLI can tell a hook-triggered run from a manual `neon checkout` / `deploy`. Read by
10
+ * {@link buildCheckoutEvent} to construct the hook contexts' `event` field (see
11
+ * `CheckoutEvent` in `@neon/config`) — not carried on {@link GitContext} itself.
12
+ */
13
+ const GIT_HOOK_ENV_FLAG = "NEON_GIT_HOOK";
14
+ /** Sentinel marking the `post-checkout` hook as neon-managed (safe to update/remove). */
15
+ const HOOK_SENTINEL = "# neon:git-sync (managed — edit `neon git` instead)";
16
+ const POST_CHECKOUT_HOOK = `#!/usr/bin/env sh
17
+ ${HOOK_SENTINEL}
18
+ # Syncs the Neon branch to the checked-out git branch. Runs only on branch switches
19
+ # ($3 == 1), never blocks the checkout, and no-ops when neon isn't on PATH.
20
+ [ "$3" = "1" ] || exit 0
21
+ command -v neon >/dev/null 2>&1 || exit 0
22
+ ${GIT_HOOK_ENV_FLAG}=1 neon git sync --quiet || true
23
+ `;
24
+ /** Run a git command, returning trimmed stdout, or `undefined` on any failure. */
25
+ const git = (args, cwd) => {
26
+ try {
27
+ return execFileSync("git", args, {
28
+ cwd,
29
+ encoding: "utf-8",
30
+ stdio: [
31
+ "ignore",
32
+ "pipe",
33
+ "ignore"
34
+ ]
35
+ }).trim();
36
+ } catch {
37
+ return;
38
+ }
39
+ };
40
+ /** Whether `cwd` is inside a git work tree. */
41
+ const isGitRepo = (cwd) => git(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
42
+ /** Absolute path to the repository root (`git rev-parse --show-toplevel`), if any. */
43
+ const gitRepoRoot = (cwd) => git(["rev-parse", "--show-toplevel"], cwd);
44
+ /** The current branch name, or `undefined` in detached-HEAD state / outside a repo. */
45
+ const currentGitBranch = (cwd) => {
46
+ const branch = git([
47
+ "symbolic-ref",
48
+ "--quiet",
49
+ "--short",
50
+ "HEAD"
51
+ ], cwd);
52
+ return branch && branch.length > 0 ? branch : void 0;
53
+ };
54
+ /** All local branch names (`refs/heads`). Empty outside a repo or with no branches. */
55
+ const localGitBranches = (cwd) => {
56
+ const out = git([
57
+ "for-each-ref",
58
+ "--format=%(refname:short)",
59
+ "refs/heads"
60
+ ], cwd);
61
+ if (!out) return [];
62
+ return out.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
63
+ };
64
+ /** Whether the current branch has a configured upstream (`@{u}`) to pull from. */
65
+ const hasUpstream = (cwd) => git([
66
+ "rev-parse",
67
+ "--abbrev-ref",
68
+ "--symbolic-full-name",
69
+ "@{u}"
70
+ ], cwd) !== void 0;
71
+ /**
72
+ * Fast-forward the current branch from its upstream (`git pull --ff-only`). Used by
73
+ * `git sync` to bring local files — crucially, committed migration files — up to date with a
74
+ * shared branch *before* a `checkout.after` migration runs, so a branch that is ahead of your
75
+ * local tree doesn't leave code↔schema skewed.
76
+ *
77
+ * `--ff-only` is deliberate: it never creates a merge commit or leaves conflict markers in
78
+ * your tree. If the branch has diverged it fails cleanly (reported, non-fatal) instead of
79
+ * mutating the working tree mid-sync. No-ops (returns `no-upstream`) when there's nothing to
80
+ * pull from.
81
+ */
82
+ const gitPull = (cwd) => {
83
+ if (!hasUpstream(cwd)) return { status: "no-upstream" };
84
+ try {
85
+ execFileSync("git", ["pull", "--ff-only"], {
86
+ cwd,
87
+ encoding: "utf-8",
88
+ stdio: [
89
+ "ignore",
90
+ "pipe",
91
+ "pipe"
92
+ ]
93
+ });
94
+ return { status: "pulled" };
95
+ } catch (err) {
96
+ let detail;
97
+ if (err !== null && typeof err === "object" && "stderr" in err && typeof err.stderr === "string" && err.stderr.trim().length > 0) detail = err.stderr.trim();
98
+ else detail = err instanceof Error ? err.message : "unknown error";
99
+ return {
100
+ status: "failed",
101
+ detail
102
+ };
103
+ }
104
+ };
105
+ /**
106
+ * Read the read-only git facts the hooks receive. Never throws: outside a repo (or with git
107
+ * uninstalled) it returns `{ available: false, … }` with the optional fields absent.
108
+ *
109
+ * @param cwd directory to inspect.
110
+ */
111
+ const readGitContext = (cwd) => {
112
+ if (!isGitRepo(cwd)) return {
113
+ available: false,
114
+ isDetached: false,
115
+ isDirty: false
116
+ };
117
+ const branch = currentGitBranch(cwd);
118
+ const sha = git(["rev-parse", "HEAD"], cwd);
119
+ const shortSha = git([
120
+ "rev-parse",
121
+ "--short",
122
+ "HEAD"
123
+ ], cwd);
124
+ const status = git(["status", "--porcelain"], cwd);
125
+ const remoteHead = git([
126
+ "rev-parse",
127
+ "--abbrev-ref",
128
+ "origin/HEAD"
129
+ ], cwd);
130
+ const defaultBranch = remoteHead?.startsWith("origin/") ? remoteHead.slice(7) : void 0;
131
+ const remoteUrl = git([
132
+ "remote",
133
+ "get-url",
134
+ "origin"
135
+ ], cwd);
136
+ const repoRoot = gitRepoRoot(cwd);
137
+ return {
138
+ available: true,
139
+ isDetached: branch === void 0,
140
+ isDirty: status !== void 0 && status.length > 0,
141
+ ...branch ? {
142
+ branch,
143
+ neonSafeBranchName: neonSafeBranchName(branch)
144
+ } : {},
145
+ ...sha ? { sha } : {},
146
+ ...shortSha ? { shortSha } : {},
147
+ ...defaultBranch ? { defaultBranch } : {},
148
+ ...remoteUrl ? { remoteUrl } : {},
149
+ ...repoRoot ? { repoRoot } : {}
150
+ };
151
+ };
152
+ /**
153
+ * Construct the `CheckoutEvent` a checkout/create hook context receives: `"git-checkout"`
154
+ * when the installed `post-checkout` hook triggered this run (env flag {@link
155
+ * GIT_HOOK_ENV_FLAG}), carrying the git branch that triggered it — `inputName` is never set
156
+ * on this variant, since nothing was typed at a prompt. `"neon-checkout"` for an explicit
157
+ * `neon checkout` invocation, carrying whatever name/id the user passed (`undefined` for
158
+ * the interactive picker). `git sync` bails out before ever reaching a checkout when `git`
159
+ * is detached/unavailable (see `sync` in `commands/git.ts`), so a defined `git.branch` is
160
+ * expected whenever the env flag is set; falling back to `""` is a defensive guard against
161
+ * that invariant breaking, not an expected runtime path.
162
+ */
163
+ const buildCheckoutEvent = (git, inputName) => process.env["NEON_GIT_HOOK"] === "1" ? {
164
+ type: "git-checkout",
165
+ gitBranch: git.branch ?? ""
166
+ } : {
167
+ type: "neon-checkout",
168
+ inputName
169
+ };
170
+ /**
171
+ * Path to the `post-checkout` hook for a repo. Uses `git rev-parse --git-path hooks` so it
172
+ * honors whatever git itself would use — `.git/hooks`, a `core.hooksPath` override (relative
173
+ * or absolute), worktrees, etc. — instead of re-deriving the rules by hand.
174
+ */
175
+ const postCheckoutHookPath = (repoRoot) => {
176
+ const hooksDir = git([
177
+ "rev-parse",
178
+ "--git-path",
179
+ "hooks"
180
+ ], repoRoot);
181
+ const dir = hooksDir ? isAbsolute(hooksDir) ? hooksDir : resolve(repoRoot, hooksDir) : resolve(repoRoot, ".git", "hooks");
182
+ return resolve(dir, "post-checkout");
183
+ };
184
+ /** Whether a `post-checkout` hook exists and is neon-managed (carries our sentinel). */
185
+ const isManagedHook = (hookPath) => {
186
+ if (!existsSync(hookPath)) return false;
187
+ try {
188
+ return readFileSync(hookPath, "utf-8").includes(HOOK_SENTINEL);
189
+ } catch {
190
+ return false;
191
+ }
192
+ };
193
+ /**
194
+ * Install (or refresh) the neon-managed `post-checkout` hook. Refuses to clobber a
195
+ * pre-existing hook we don't own, returning `{ status: 'conflict' }` so the caller can warn
196
+ * instead of silently overwriting the user's script.
197
+ */
198
+ const installPostCheckoutHook = (repoRoot) => {
199
+ const hookPath = postCheckoutHookPath(repoRoot);
200
+ const existed = existsSync(hookPath);
201
+ if (existed && !isManagedHook(hookPath)) return {
202
+ status: "conflict",
203
+ hookPath
204
+ };
205
+ mkdirSync(dirname(hookPath), { recursive: true });
206
+ writeFileSync(hookPath, POST_CHECKOUT_HOOK);
207
+ chmodSync(hookPath, 493);
208
+ return { status: existed ? "updated" : "installed" };
209
+ };
210
+ /** Remove the managed `post-checkout` hook. Leaves a foreign (user-authored) hook untouched. */
211
+ const removePostCheckoutHook = (repoRoot) => {
212
+ const hookPath = postCheckoutHookPath(repoRoot);
213
+ if (!existsSync(hookPath)) return "absent";
214
+ if (!isManagedHook(hookPath)) return "foreign";
215
+ rmSync(hookPath);
216
+ log.debug("Removed managed post-checkout hook at %s", hookPath);
217
+ return "removed";
218
+ };
219
+ //#endregion
220
+ export { GIT_HOOK_ENV_FLAG, buildCheckoutEvent, currentGitBranch, gitPull, gitRepoRoot, hasUpstream, installPostCheckoutHook, isGitRepo, isManagedHook, localGitBranches, postCheckoutHookPath, readGitContext, removePostCheckoutHook };
@@ -0,0 +1,230 @@
1
+ import { log } from "../log.js";
2
+ import { s as fetchEnvKeys } from "../_chunks/env-N19A7qND.js";
3
+ import { loadConfigFromFile, runHook } from "@neon/config-runtime";
4
+ //#region src/utils/hooks.ts
5
+ /**
6
+ * Load the nearest `neon.ts` for hook discovery, or `undefined` when there is none — or when
7
+ * it fails to load. Deliberately best-effort: `checkout` on an *existing* branch must keep
8
+ * working even with a broken `neon.ts` sitting in the repo, exactly as it did before hooks
9
+ * existed. A load failure other than "missing" degrades to a warning rather than aborting
10
+ * the checkout; a genuinely broken policy still surfaces loudly the moment something that
11
+ * actually depends on it runs — `checkout --create`'s own `createBranchFromPolicyOnCheckout`,
12
+ * or `deploy`, both load the config again through their own (throwing) path. Callers read
13
+ * `.experimental?.hooks` off the result for the hooks block, and pass the whole thing to
14
+ * {@link resolveHookEnv} so `after` hooks resolve exactly the namespaces the policy declares.
15
+ */
16
+ const loadHookConfig = async (cwd) => {
17
+ try {
18
+ const { config } = await loadConfigFromFile({ cwd });
19
+ return config;
20
+ } catch (err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ if (!/Could not find a Neon config file/i.test(message)) log.warning("Could not load neon.ts for lifecycle hooks (continuing without them): %s", message);
23
+ return;
24
+ }
25
+ };
26
+ /** Stream hook (shell) output through the CLI logger. */
27
+ const onOutput = (chunk) => {
28
+ const text = chunk.replace(/\n$/, "");
29
+ if (text.length > 0) log.info("%s", text);
30
+ };
31
+ /**
32
+ * Run the `checkout.before` hook (if any). Returns the rewritten branch name when the hook
33
+ * (function form) returns `{ name }`, else `undefined`. Throws propagate to abort the
34
+ * checkout — that's the documented `before`-hook contract.
35
+ */
36
+ const runCheckoutBeforeHook = async (args) => {
37
+ const hook = args.hooks?.checkout?.before;
38
+ if (!hook) return void 0;
39
+ return (await runHook(hook, {
40
+ event: args.event,
41
+ git: args.git
42
+ }, {
43
+ cwd: args.cwd,
44
+ onOutput
45
+ }))?.name;
46
+ };
47
+ /**
48
+ * Run the `create.before` hook (if any). Fires only when a new branch is actually about to
49
+ * be created (never for `checkout` selecting an existing one). Throws propagate to abort
50
+ * the create — the checkout that triggered it aborts too.
51
+ */
52
+ const runCreateBeforeHook = async (args) => {
53
+ const hook = args.hooks?.create?.before;
54
+ if (!hook) return;
55
+ await runHook(hook, {
56
+ branchName: args.branchName,
57
+ git: args.git,
58
+ event: args.event
59
+ }, {
60
+ cwd: args.cwd,
61
+ onOutput
62
+ });
63
+ };
64
+ /** Run the `deploy.before` hook (if any). Throws propagate to abort the deploy. */
65
+ const runDeployBeforeHook = async (args) => {
66
+ const hook = args.hooks?.deploy?.before;
67
+ if (!hook) return;
68
+ await runHook(hook, {
69
+ branch: args.branch,
70
+ git: args.git,
71
+ event: args.event
72
+ }, {
73
+ cwd: args.cwd,
74
+ onOutput
75
+ });
76
+ };
77
+ /**
78
+ * Run the `checkout.after` hook (if any). Failures degrade to a warning — the branch is
79
+ * already checked out, so an `after` failure must not unwind the pin.
80
+ */
81
+ const runCheckoutAfterHook = async (args) => {
82
+ const hook = args.hooks?.checkout?.after;
83
+ if (!hook) return;
84
+ await runAfter("checkout.after", () => runHook(hook, {
85
+ branch: args.branch,
86
+ env: args.env,
87
+ git: args.git,
88
+ event: args.event
89
+ }, {
90
+ cwd: args.cwd,
91
+ env: hookEnvToProcessEnv(args.env),
92
+ onOutput
93
+ }));
94
+ };
95
+ /**
96
+ * Run the `create.after` hook (if any). Failures degrade to a warning — the branch already
97
+ * exists, so an `after` failure must not unwind the create.
98
+ */
99
+ const runCreateAfterHook = async (args) => {
100
+ const hook = args.hooks?.create?.after;
101
+ if (!hook) return;
102
+ await runAfter("create.after", () => runHook(hook, {
103
+ branch: args.branch,
104
+ env: args.env,
105
+ git: args.git,
106
+ event: args.event
107
+ }, {
108
+ cwd: args.cwd,
109
+ env: hookEnvToProcessEnv(args.env),
110
+ onOutput
111
+ }));
112
+ };
113
+ /** Run the `deploy.after` hook (if any). Failures degrade to a warning (apply already ran). */
114
+ const runDeployAfterHook = async (args) => {
115
+ const hook = args.hooks?.deploy?.after;
116
+ if (!hook) return;
117
+ await runAfter("deploy.after", () => runHook(hook, {
118
+ branch: args.branch,
119
+ env: args.env,
120
+ result: args.result,
121
+ git: args.git,
122
+ event: args.event
123
+ }, {
124
+ cwd: args.cwd,
125
+ env: hookEnvToProcessEnv(args.env),
126
+ onOutput
127
+ }));
128
+ };
129
+ const runAfter = async (label, run) => {
130
+ try {
131
+ await run();
132
+ } catch (err) {
133
+ const message = err instanceof Error ? err.message : String(err);
134
+ log.warning("The `%s` hook failed: %s\nThe branch change already succeeded; re-run the hook manually if needed.", label, message);
135
+ }
136
+ };
137
+ /**
138
+ * Fetch the branch's resolved Neon env (DATABASE_URL, …) for an `after` hook, shaped into
139
+ * {@link HookEnv}. Resolved in-memory regardless of `--env-pull`, so a migration hook always
140
+ * has a connection string even when no `.env` is written. Returns `undefined` on failure so
141
+ * the caller can skip the hook with a warning rather than crash.
142
+ *
143
+ * `config` is the same policy the checkout/deploy is already working from — it decides
144
+ * which namespaces `fetchEnvKeys` resolves (an empty/default `Config` would resolve
145
+ * postgres only, since nothing else is declared enabled).
146
+ */
147
+ const resolveHookEnv = async (config, options) => {
148
+ try {
149
+ const resolved = await fetchEnvKeys(config, options, null);
150
+ return shapeHookEnv(resolved);
151
+ } catch (err) {
152
+ const message = err instanceof Error ? err.message : String(err);
153
+ log.warning("Could not resolve env for the after hook: %s", message);
154
+ return;
155
+ }
156
+ };
157
+ /**
158
+ * Map `@neon-internals/env-core`'s flat, all-optional {@link ResolvedNeonEnv} into
159
+ * {@link HookEnv}. Pure — split out from {@link resolveHookEnv} so the shaping is testable
160
+ * without a live Neon API call. `postgres` always defaults to empty strings rather than
161
+ * being dropped: {@link HookEnv.postgres} is the one namespace every hook can rely on
162
+ * existing, even when this particular fetch didn't select either connection string.
163
+ */
164
+ const shapeHookEnv = (resolved) => ({
165
+ postgres: {
166
+ databaseUrl: resolved.postgres?.databaseUrl ?? "",
167
+ databaseUrlUnpooled: resolved.postgres?.databaseUrlUnpooled ?? ""
168
+ },
169
+ ...resolved.branch?.name ? { branch: { name: resolved.branch.name } } : {},
170
+ ...resolved.auth?.baseUrl !== void 0 && resolved.auth.jwksUrl !== void 0 ? { auth: {
171
+ baseUrl: resolved.auth.baseUrl,
172
+ jwksUrl: resolved.auth.jwksUrl
173
+ } } : {},
174
+ ...resolved.dataApi?.url !== void 0 ? { dataApi: { url: resolved.dataApi.url } } : {},
175
+ ...resolved.storage?.accessKeyId !== void 0 && resolved.storage.secretAccessKey !== void 0 && resolved.storage.endpoint !== void 0 && resolved.storage.region !== void 0 ? { storage: {
176
+ accessKeyId: resolved.storage.accessKeyId,
177
+ secretAccessKey: resolved.storage.secretAccessKey,
178
+ endpoint: resolved.storage.endpoint,
179
+ region: resolved.storage.region
180
+ } } : {},
181
+ ...resolved.aiGateway?.apiKey !== void 0 && resolved.aiGateway.baseUrl !== void 0 ? { aiGateway: {
182
+ apiKey: resolved.aiGateway.apiKey,
183
+ baseUrl: resolved.aiGateway.baseUrl
184
+ } } : {},
185
+ ...resolved.functions ? { functions: Object.fromEntries(Object.entries(resolved.functions).map(([slug, fn]) => [slug, { baseUrl: fn.baseUrl }])) } : {}
186
+ });
187
+ /** Re-derive the OS-level env vars from a {@link HookEnv} for shell-hook injection. */
188
+ const hookEnvToProcessEnv = (env) => {
189
+ const out = {
190
+ DATABASE_URL: env.postgres.databaseUrl,
191
+ DATABASE_URL_UNPOOLED: env.postgres.databaseUrlUnpooled
192
+ };
193
+ if (env.branch?.name) out.NEON_BRANCH = env.branch.name;
194
+ if (env.auth) {
195
+ out.NEON_AUTH_BASE_URL = env.auth.baseUrl;
196
+ out.NEON_AUTH_JWKS_URL = env.auth.jwksUrl;
197
+ }
198
+ if (env.dataApi) out.NEON_DATA_API_URL = env.dataApi.url;
199
+ if (env.storage) {
200
+ out.AWS_ACCESS_KEY_ID = env.storage.accessKeyId;
201
+ out.AWS_SECRET_ACCESS_KEY = env.storage.secretAccessKey;
202
+ out.AWS_ENDPOINT_URL_S3 = env.storage.endpoint;
203
+ out.AWS_REGION = env.storage.region;
204
+ }
205
+ if (env.aiGateway) {
206
+ out.NEON_AI_GATEWAY_TOKEN = env.aiGateway.apiKey;
207
+ out.NEON_AI_GATEWAY_BASE_URL = env.aiGateway.baseUrl;
208
+ }
209
+ return out;
210
+ };
211
+ /**
212
+ * Build a {@link HookBranch} from the live branch metadata. `created` is supplied by the
213
+ * caller (only the checkout/deploy flow knows whether this op created the branch).
214
+ */
215
+ const buildHookBranch = async (args) => {
216
+ const { data } = await args.apiClient.getProjectBranch(args.projectId, args.branchId);
217
+ const branch = data.branch;
218
+ return {
219
+ projectId: args.projectId,
220
+ id: branch.id,
221
+ name: branch.name,
222
+ created: args.created,
223
+ isDefault: branch.default ?? false,
224
+ isProtected: branch.protected ?? false,
225
+ ...branch.parent_id ? { parentId: branch.parent_id } : {},
226
+ ...branch.expires_at ? { expiresAt: branch.expires_at } : {}
227
+ };
228
+ };
229
+ //#endregion
230
+ export { buildHookBranch, loadHookConfig, resolveHookEnv, runCheckoutAfterHook, runCheckoutBeforeHook, runCreateAfterHook, runCreateBeforeHook, runDeployAfterHook, runDeployBeforeHook, shapeHookEnv };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "6.0.0",
3
+ "version": "6.2.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -64,9 +64,9 @@
64
64
  "yaml": "^2.9.0",
65
65
  "yargs": "17.7.2",
66
66
  "yoctocolors": "^2.1.2",
67
- "@neon/config": "1.7.3",
68
- "@neon/config-runtime": "1.6.3",
69
- "@neon/sdk": "6.1.0"
67
+ "@neon/sdk": "6.1.0",
68
+ "@neon/config": "1.8.0",
69
+ "@neon/config-runtime": "1.7.0"
70
70
  },
71
71
  "optionalDependencies": {
72
72
  "@napi-rs/keyring": "1.3.0",
@@ -100,7 +100,7 @@
100
100
  "typescript": "^5.9.0",
101
101
  "vitest": "^3.0.9",
102
102
  "@neon-internals/cli-core": "0.0.0",
103
- "@neon-internals/env-core": "0.0.22",
103
+ "@neon-internals/env-core": "0.0.23",
104
104
  "@neon/e2e-harness": "0.0.0",
105
105
  "@neon/functions": "0.11.0"
106
106
  },
@@ -1,49 +0,0 @@
1
- import { InitCancelled } from "./cancelled.js";
2
- import { spawn } from "node:child_process";
3
- //#region src/init/child.ts
4
- const AUTH_CHILD = /* @__PURE__ */ new Set([
5
- "bootstrap",
6
- "link",
7
- "mcp",
8
- "env"
9
- ]);
10
- const initChildEnv = (command, overlay, base = process.env) => {
11
- const child = { ...base };
12
- if (command === void 0 || !AUTH_CHILD.has(command)) {
13
- for (const key of Object.keys(child)) if (key.toUpperCase() === "NEON_API_KEY") delete child[key];
14
- }
15
- return overlay ? {
16
- ...child,
17
- ...overlay
18
- } : child;
19
- };
20
- const spawnCliChild = async (argv, cwd, overlay) => {
21
- const cli = process.argv[1];
22
- if (!cli) throw new Error("Cannot re-exec the Neon CLI: process.argv[1] is missing.");
23
- return new Promise((resolve, reject) => {
24
- const child = spawn(process.execPath, [cli, ...argv], {
25
- cwd,
26
- stdio: "inherit",
27
- env: initChildEnv(argv[0], overlay)
28
- });
29
- const onSigint = () => {
30
- child.kill("SIGINT");
31
- };
32
- process.on("SIGINT", onSigint);
33
- const finish = (code, signal) => {
34
- process.off("SIGINT", onSigint);
35
- if (signal === "SIGINT" || signal === "SIGTERM") {
36
- reject(new InitCancelled());
37
- return;
38
- }
39
- resolve(code === 0);
40
- };
41
- child.on("error", () => {
42
- process.off("SIGINT", onSigint);
43
- resolve(false);
44
- });
45
- child.on("close", finish);
46
- });
47
- };
48
- //#endregion
49
- export { AUTH_CHILD, initChildEnv, spawnCliChild };