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.
@@ -0,0 +1,356 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { isCi } from "../env.js";
3
+ import { log } from "../log.js";
4
+ import { CONTEXT_FILE, contextBranch, gitBranchMap, gitBranchMapping, isUnfollowedGitHookSync, readContextFile, setGitBranchMap, setGitBranchMapping, setGitFollow } from "../context.js";
5
+ import { writer } from "../writer.js";
6
+ import { neonSafeBranchName } from "../utils/branch_name.js";
7
+ import { currentGitBranch, gitPull, gitRepoRoot, hasUpstream, installPostCheckoutHook, isGitRepo, isManagedHook, localGitBranches, postCheckoutHookPath, readGitContext, removePostCheckoutHook } from "../utils/git.js";
8
+ import { quoteFlagValue } from "./link.js";
9
+ import { handler as handler$1 } from "./checkout.js";
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import chalk from "chalk";
13
+ import prompts from "prompts";
14
+ //#region src/commands/git.ts
15
+ var git_exports = /* @__PURE__ */ __exportAll({
16
+ aliases: () => aliases,
17
+ builder: () => builder,
18
+ cleanup: () => cleanup,
19
+ command: () => "git",
20
+ describe: () => describe,
21
+ handler: () => handler,
22
+ install: () => install,
23
+ partitionBranchesToPrune: () => partitionBranchesToPrune,
24
+ status: () => status,
25
+ sync: () => sync,
26
+ uninstall: () => uninstall
27
+ });
28
+ const command = "git";
29
+ const describe = "Sync the checked-out Neon branch to your git branch (Preview)";
30
+ const aliases = [];
31
+ const builder = (argv) => argv.usage("$0 git <sub-command> [options]").command("install", "Install a git post-checkout hook that syncs the Neon branch on `git checkout`", (yargs) => yargs.epilogue("Requires a project already linked at the repository root — run `neon link` or `neon checkout <branch>` there first."), (args) => {
32
+ install(args);
33
+ }).command("uninstall", "Remove the git post-checkout hook installed by `git install`", (yargs) => yargs, (args) => {
34
+ uninstall(args);
35
+ }).command("sync", "Check out the Neon branch mapped to the current git branch (run by the hook)", (yargs) => yargs.options({
36
+ "env-pull": {
37
+ describe: "Pull the branch's Neon env vars into a local .env after sync. On by default.",
38
+ type: "boolean",
39
+ default: true
40
+ },
41
+ pull: {
42
+ describe: "Run `git pull --ff-only` before syncing so local files (incl. migration files) match the branch before any checkout.after migration runs. Without the flag, prompts when run manually in a TTY and skips in the hook / CI.",
43
+ type: "boolean"
44
+ },
45
+ quiet: {
46
+ describe: "Suppress sync's own routine status lines (git pull result, detached HEAD notice). Warnings and the delegated checkout's own output are unaffected. Used by the installed git hook.",
47
+ type: "boolean",
48
+ default: false
49
+ }
50
+ }), (args) => sync(args)).command("status", "Show the git context, hook state, and current git → Neon mapping", (yargs) => yargs, (args) => {
51
+ status(args);
52
+ }).command("cleanup", "Prune git → Neon mappings whose local git branch is gone; optionally delete the orphaned Neon branches", (yargs) => yargs.options({
53
+ "prune-neon-branches": {
54
+ describe: "Also delete the orphaned Neon branches (never the default or a protected branch).",
55
+ type: "boolean",
56
+ default: false
57
+ },
58
+ yes: {
59
+ describe: "Skip the confirmation prompt before deleting Neon branches.",
60
+ type: "boolean",
61
+ default: false
62
+ }
63
+ }), (args) => cleanup(args));
64
+ const handler = (args) => args;
65
+ const requireRepoRoot = () => {
66
+ const cwd = process.cwd();
67
+ if (!isGitRepo(cwd)) throw new Error("Not inside a git repository. Run `neon git` from a git work tree.");
68
+ const repoRoot = gitRepoRoot(cwd);
69
+ if (!repoRoot) throw new Error("Could not resolve the git repository root.");
70
+ return repoRoot;
71
+ };
72
+ /**
73
+ * `.neon` at exactly the repo root — never walk-up-resolved. The single source of truth for
74
+ * all git-hooks state: `git.follow` and the git → Neon map. A `post-checkout` hook always
75
+ * runs with `cwd` at the repo root, so every function here (`install`, `uninstall`, `sync`,
76
+ * `status`, `cleanup`) reads and writes this exact path, never `props.contextFile`'s
77
+ * walk-up resolution — an ancestor directory's unrelated `.neon` (e.g. a parent monorepo
78
+ * folder) must never be mistaken for this repo's own state.
79
+ *
80
+ * Deliberately never *creates* this file: `install` requires it to already exist (from a
81
+ * prior `neon link` / `neon checkout` run at the repo root), and `sync` skips persisting a
82
+ * mapping when it's absent. Auto-creating a bare `{ git: {...} }` file with no `projectId`
83
+ * would plant a **new**, closer `.neon` that shadows a real project link recorded in an
84
+ * ancestor directory (or a linked subdirectory) for every other command's normal
85
+ * context resolution — silently breaking `checkout` / `deploy` / `status` in that
86
+ * directory. Requiring the link first also keeps this file and the project context file
87
+ * the same file whenever hooks state exists, which is what makes `cleanup`'s project-match
88
+ * check (see below) actually correct.
89
+ */
90
+ const repoRootContextFile = (repoRoot) => join(repoRoot, CONTEXT_FILE);
91
+ const install = (_props) => {
92
+ const repoRoot = requireRepoRoot();
93
+ const gitStateFile = repoRootContextFile(repoRoot);
94
+ if (!readContextFile(gitStateFile).projectId) {
95
+ const quoted = quoteFlagValue(gitStateFile);
96
+ throw new Error(`No project linked at the repository root (${gitStateFile}).\nRun \`neon link --context-file ${quoted}\` (an explicit path, since a plain \`neon link\` walks up and may update a different, ancestor \`.neon\` instead) or \`neon checkout <branch> --context-file ${quoted}\` first, then re-run \`neon git install\`.`);
97
+ }
98
+ const result = installPostCheckoutHook(repoRoot);
99
+ if (result.status === "conflict") throw new Error(`A non-neon \`post-checkout\` hook already exists at ${result.hookPath}.\nRemove or rename it first, then re-run \`neon git install\`.`);
100
+ setGitFollow(gitStateFile, true);
101
+ log.info("Git → Neon sync %s. `git checkout <branch>` will now check out the mapped Neon branch.\nHook: %s", result.status === "installed" ? "installed" : "updated", postCheckoutHookPath(repoRoot));
102
+ };
103
+ const uninstall = (_props) => {
104
+ const repoRoot = requireRepoRoot();
105
+ const result = removePostCheckoutHook(repoRoot);
106
+ const gitStateFile = repoRootContextFile(repoRoot);
107
+ if (existsSync(gitStateFile)) setGitFollow(gitStateFile, false);
108
+ switch (result) {
109
+ case "removed":
110
+ log.info("Removed the neon git post-checkout hook. Git → Neon sync is off.");
111
+ break;
112
+ case "foreign":
113
+ log.warning("Left the existing `post-checkout` hook in place (not managed by neon). Git → Neon sync flag cleared.");
114
+ break;
115
+ case "absent": log.info("No neon git hook was installed. Git → Neon sync flag cleared.");
116
+ }
117
+ };
118
+ /**
119
+ * Check out the Neon branch that corresponds to the current git branch. Invoked by the
120
+ * installed `post-checkout` hook (with `NEON_GIT_HOOK=1`) and runnable by hand. Resolves the
121
+ * Neon branch name via the persisted map (sticky), delegates to `neon checkout` (whose
122
+ * `checkout.before` hook may further map the name and whose `checkout.after` hook runs
123
+ * migrations, etc.), then records the resulting git → Neon mapping so it stays stable.
124
+ */
125
+ /** `sync`'s own routine status lines (pull result, detached HEAD) — skipped under
126
+ * `--quiet`. Warnings, errors, and the delegated `checkout`'s own output are unaffected. */
127
+ const syncLog = (props, ...args) => {
128
+ if (props.quiet) return;
129
+ log.info(...args);
130
+ };
131
+ const sync = async (props) => {
132
+ if (isUnfollowedGitHookSync({ _: ["git", "sync"] })) {
133
+ log.debug("Skipping git sync: this repo has not run `neon git install` (git.follow is not set).");
134
+ return;
135
+ }
136
+ const cwd = process.cwd();
137
+ if (!isGitRepo(cwd)) throw new Error("Not inside a git repository.");
138
+ const repoRoot = gitRepoRoot(cwd) ?? cwd;
139
+ const gitStateFile = repoRootContextFile(repoRoot);
140
+ const context = readContextFile(gitStateFile);
141
+ const gitBranch = currentGitBranch(cwd);
142
+ if (!gitBranch) {
143
+ syncLog(props, "Detached HEAD — no git branch to sync. Skipping.");
144
+ return;
145
+ }
146
+ if (await resolveShouldPull(props, cwd, gitBranch)) {
147
+ const outcome = gitPull(cwd);
148
+ switch (outcome.status) {
149
+ case "pulled":
150
+ syncLog(props, "%s git pull --ff-only (%s)", chalk.dim("→"), gitBranch);
151
+ break;
152
+ case "no-upstream":
153
+ syncLog(props, "No upstream for %s — skipping git pull.", gitBranch);
154
+ break;
155
+ case "failed": log.warning("git pull --ff-only failed (continuing with sync): %s", outcome.detail);
156
+ }
157
+ }
158
+ const inputName = gitBranchMapping(context, gitBranch) ?? neonSafeBranchName(gitBranch);
159
+ await handler$1({
160
+ ...props,
161
+ id: inputName,
162
+ envPull: props.envPull ?? true
163
+ });
164
+ const rootProjectId = readContextFile(gitStateFile).projectId;
165
+ const pinnedContext = readContextFile(props.contextFile);
166
+ if (!rootProjectId || pinnedContext.projectId !== rootProjectId) log.warning("Not persisting the git → Neon mapping: this checkout targeted project %s, not a project linked at the repo root (%s).", pinnedContext.projectId ?? "(unknown)", rootProjectId ?? "none");
167
+ else {
168
+ const resolved = contextBranch(pinnedContext);
169
+ if (resolved) setGitBranchMapping(gitStateFile, gitBranch, resolved);
170
+ }
171
+ };
172
+ /**
173
+ * Decide whether `git sync` should `git pull` first. Mirrors the rest of the CLI's
174
+ * auto-vs-interactive philosophy (cf. `checkout` / `link`): an explicit flag always wins;
175
+ * otherwise we only *prompt* in a real manual TTY, and never auto-pull in the post-checkout
176
+ * hook or CI (no network surprises in automation). Skips when there's nothing upstream.
177
+ */
178
+ const resolveShouldPull = async (props, cwd, gitBranch) => {
179
+ if (props.pull === true) return true;
180
+ if (props.pull === false) return false;
181
+ if (!hasUpstream(cwd)) return false;
182
+ if (process.env["NEON_GIT_HOOK"] === "1" || isCi() || !process.stdout.isTTY) return false;
183
+ const { pull } = await prompts({
184
+ type: "confirm",
185
+ name: "pull",
186
+ message: `Pull latest for "${gitBranch}" (git pull --ff-only) before syncing?`,
187
+ initial: false
188
+ });
189
+ return Boolean(pull);
190
+ };
191
+ const GIT_STATUS_FIELDS = [
192
+ "gitBranch",
193
+ "hookInstalled",
194
+ "followOnCheckout",
195
+ "mappedNeonBranch",
196
+ "mappings"
197
+ ];
198
+ const status = (props) => {
199
+ const cwd = process.cwd();
200
+ const git = readGitContext(cwd);
201
+ if (!git.available) throw new Error("Not inside a git repository.");
202
+ const repoRoot = git.repoRoot ?? cwd;
203
+ const hookPath = postCheckoutHookPath(repoRoot);
204
+ const context = readContextFile(repoRootContextFile(repoRoot));
205
+ const currentBranch = git.branch;
206
+ writer(props).end({
207
+ gitBranch: currentBranch ?? null,
208
+ hookInstalled: isManagedHook(hookPath),
209
+ followOnCheckout: context.git?.follow === true,
210
+ mappedNeonBranch: currentBranch ? gitBranchMapping(context, currentBranch) ?? null : null,
211
+ mappings: context.git?.map ?? {}
212
+ }, {
213
+ fields: GIT_STATUS_FIELDS,
214
+ renderColumns: {
215
+ gitBranch: (v) => v.gitBranch ?? "(detached)",
216
+ mappedNeonBranch: (v) => v.gitBranch === null ? "(detached — sync skips detached HEAD)" : v.mappedNeonBranch ?? "(unmapped — will derive on next sync)",
217
+ mappings: (v) => Object.entries(v.mappings).map(([gitBranch, neonBranch]) => `${gitBranch} → ${neonBranch}`).join("\n") || "(none)"
218
+ }
219
+ });
220
+ };
221
+ /**
222
+ * Split the orphaned Neon branches (those mapped from now-deleted git branches) into the ones
223
+ * safe to delete and the ones to skip. Default and protected branches are **never** deleted.
224
+ * Pure (no I/O) so it's unit-testable.
225
+ */
226
+ const partitionBranchesToPrune = (branches, orphanNeonNames) => {
227
+ const toDelete = [];
228
+ const skipped = [];
229
+ for (const branch of branches) {
230
+ if (!orphanNeonNames.has(branch.name)) continue;
231
+ if (branch.default) skipped.push({
232
+ name: branch.name,
233
+ reason: "default branch"
234
+ });
235
+ else if (branch.protected) skipped.push({
236
+ name: branch.name,
237
+ reason: "protected"
238
+ });
239
+ else toDelete.push(branch);
240
+ }
241
+ return {
242
+ toDelete,
243
+ skipped
244
+ };
245
+ };
246
+ /**
247
+ * Prune the git → Neon workflow state. By default this only cleans `.neon`: mapping entries
248
+ * whose git branch no longer exists locally are dropped. With `--prune-neon-branches` it also
249
+ * deletes the orphaned Neon branches — never the default branch and never a protected one.
250
+ */
251
+ const cleanup = async (props) => {
252
+ const cwd = process.cwd();
253
+ if (!isGitRepo(cwd)) throw new Error("Not inside a git repository.");
254
+ const repoRoot = gitRepoRoot(cwd) ?? cwd;
255
+ const gitStateFile = repoRootContextFile(repoRoot);
256
+ const context = readContextFile(gitStateFile);
257
+ const map = gitBranchMap(context);
258
+ const entries = Object.entries(map);
259
+ if (entries.length === 0) {
260
+ log.info("No git → Neon mappings to clean up.");
261
+ return;
262
+ }
263
+ const local = new Set(localGitBranches(cwd));
264
+ const stale = entries.filter(([gitBranch]) => !local.has(gitBranch));
265
+ if (stale.length === 0) {
266
+ log.info("All %d mapping(s) still have a local git branch — nothing to prune.", entries.length);
267
+ return;
268
+ }
269
+ const kept = Object.fromEntries(entries.filter(([gitBranch]) => local.has(gitBranch)));
270
+ const keptNeonNames = new Set(Object.values(kept));
271
+ const staleButShared = stale.filter(([, neonBranch]) => keptNeonNames.has(neonBranch));
272
+ const orphaned = stale.filter(([, neonBranch]) => !keptNeonNames.has(neonBranch));
273
+ if (!props.pruneNeonBranches) {
274
+ setGitBranchMap(gitStateFile, kept);
275
+ log.info("Pruned %d stale mapping(s) from .neon:", stale.length);
276
+ for (const [gitBranch, neonBranch] of stale) log.info(" %s → %s", gitBranch, neonBranch);
277
+ if (orphaned.length > 0) log.info("These Neon branch(es) are no longer mapped and were not deleted: %s. Delete them with `neon branches delete <name>`, or run `neon git cleanup --prune-neon-branches` next time before a mapping-only cleanup.", orphaned.map(([, neonBranch]) => neonBranch).join(", "));
278
+ return;
279
+ }
280
+ if (staleButShared.length > 0) {
281
+ setGitBranchMap(gitStateFile, {
282
+ ...kept,
283
+ ...Object.fromEntries(orphaned)
284
+ });
285
+ log.info("Pruned %d stale mapping(s) whose Neon branch is still in use elsewhere:", staleButShared.length);
286
+ for (const [gitBranch, neonBranch] of staleButShared) log.info(" %s → %s", gitBranch, neonBranch);
287
+ }
288
+ if (!props.projectId) throw new Error("Cannot delete Neon branches: no project in context. Run `neon link` first.");
289
+ if (!context.projectId) throw new Error(`Cannot delete Neon branches: the git → Neon map has no recorded project (the repo-root .neon at ${gitStateFile} has no projectId). Run \`neon link --context-file ${quoteFlagValue(gitStateFile)}\` to establish it, or \`neon git cleanup\` with no --prune-neon-branches to only prune the local mapping.`);
290
+ if (context.projectId !== props.projectId) throw new Error(`Cannot delete Neon branches: this git → Neon map was recorded against project ${context.projectId}, but the active project is ${props.projectId}. Run \`neon link\` (or drop --project-id) to target ${context.projectId}, or \`neon git cleanup\` with no --prune-neon-branches to only prune the local mapping.`);
291
+ if (orphaned.length === 0) {
292
+ log.info("No orphaned Neon branches to delete.");
293
+ return;
294
+ }
295
+ const orphanNeonNames = new Set(orphaned.map(([, neonBranch]) => neonBranch));
296
+ const branches = (await props.apiClient.listProjectBranches({ projectId: props.projectId })).data.branches;
297
+ const { toDelete, skipped } = partitionBranchesToPrune(branches, orphanNeonNames);
298
+ for (const { name, reason } of skipped) log.warning("Keeping Neon branch %s (%s) — never auto-deleted.", name, reason);
299
+ if (toDelete.length === 0) {
300
+ log.info("No orphaned Neon branches to delete.");
301
+ return;
302
+ }
303
+ if (!await confirmPrune(props, toDelete)) {
304
+ log.info("Aborted — no Neon branches were deleted, and no mappings were pruned.");
305
+ return;
306
+ }
307
+ const currentMap = {
308
+ ...kept,
309
+ ...Object.fromEntries(orphaned)
310
+ };
311
+ const failed = [];
312
+ let prunedCount = 0;
313
+ for (const branch of toDelete) {
314
+ const branchMappings = orphaned.filter(([, neonBranch]) => neonBranch === branch.name);
315
+ try {
316
+ await props.apiClient.deleteProjectBranch(props.projectId, branch.id);
317
+ log.info("Deleted Neon branch %s (%s).", branch.name, branch.id);
318
+ prunedCount += branchMappings.length;
319
+ for (const [gitBranch, neonBranch] of branchMappings) {
320
+ log.info(" Pruned %s → %s", gitBranch, neonBranch);
321
+ delete currentMap[gitBranch];
322
+ }
323
+ } catch (err) {
324
+ const message = err instanceof Error ? err.message : String(err);
325
+ log.error("Failed to delete Neon branch %s: %s", branch.name, message);
326
+ failed.push({
327
+ name: branch.name,
328
+ error: message
329
+ });
330
+ }
331
+ setGitBranchMap(gitStateFile, currentMap);
332
+ }
333
+ if (prunedCount > 0) log.info("Pruned %d stale mapping(s) from .neon.", prunedCount);
334
+ if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Neon branch(es): ${failed.map((f) => `${f.name} (${f.error})`).join(", ")}. Re-run \`neon git cleanup --prune-neon-branches\` to retry.`);
335
+ };
336
+ /**
337
+ * Confirm deleting the orphaned Neon branches. Deletion is destructive, so an explicit
338
+ * `--yes` wins; otherwise prompt in an interactive terminal, and refuse (with a warning) in
339
+ * CI / non-interactive contexts rather than deleting without consent.
340
+ */
341
+ const confirmPrune = async (props, toDelete) => {
342
+ if (props.yes) return true;
343
+ if (isCi() || !process.stdout.isTTY) {
344
+ log.warning("Refusing to delete %d Neon branch(es) non-interactively. Re-run with --yes to confirm.", toDelete.length);
345
+ return false;
346
+ }
347
+ const { ok } = await prompts({
348
+ type: "confirm",
349
+ name: "ok",
350
+ message: `Delete ${toDelete.length} orphaned Neon branch(es): ${toDelete.map((branch) => branch.name).join(", ")}?`,
351
+ initial: false
352
+ });
353
+ return Boolean(ok);
354
+ };
355
+ //#endregion
356
+ export { aliases, builder, cleanup, command, describe, handler, install, partitionBranchesToPrune, status, sync, git_exports as t, uninstall };
@@ -6,6 +6,9 @@ import { t as env_exports } from "./env.js";
6
6
  import { t as config_exports } from "./config.js";
7
7
  import { t as projects_exports } from "./projects.js";
8
8
  import { t as link_exports } from "./link.js";
9
+ import { t as mcp_exports } from "./mcp.js";
10
+ import { t as plugins_exports } from "./plugins.js";
11
+ import { t as skills_exports } from "./skills.js";
9
12
  import { t as bootstrap_exports } from "./bootstrap.js";
10
13
  import { t as branches_exports } from "./branches.js";
11
14
  import { t as bucket_exports } from "./bucket.js";
@@ -19,21 +22,19 @@ import { t as deploy_exports } from "./deploy.js";
19
22
  import { t as dev_exports } from "./dev.js";
20
23
  import { t as diff_exports } from "./diff.js";
21
24
  import { t as functions_exports } from "./functions.js";
25
+ import { t as git_exports } from "./git.js";
22
26
  import { t as init_exports } from "./init.js";
23
27
  import { t as inspect_exports } from "./inspect.js";
24
28
  import { t as ip_allow_exports } from "./ip_allow.js";
25
29
  import { t as logs_exports } from "./logs.js";
26
- import { t as mcp_exports } from "./mcp.js";
27
30
  import { t as neon_auth_exports } from "./neon_auth.js";
28
31
  import { t as open_exports } from "./open.js";
29
32
  import { t as operations_exports } from "./operations.js";
30
33
  import { t as orgs_exports } from "./orgs.js";
31
- import { t as plugins_exports } from "./plugins.js";
32
34
  import { t as profile_exports } from "./profile.js";
33
35
  import { t as psql_exports } from "./psql.js";
34
36
  import { t as roles_exports } from "./roles.js";
35
37
  import { t as set_context_exports } from "./set_context.js";
36
- import { t as skills_exports } from "./skills.js";
37
38
  import { t as snapshots_exports } from "./snapshots.js";
38
39
  import { t as status_exports } from "./status.js";
39
40
  import { t as triggers_exports } from "./triggers.js";
@@ -62,6 +63,7 @@ var commands_default = [
62
63
  psql_exports,
63
64
  set_context_exports,
64
65
  checkout_exports,
66
+ git_exports,
65
67
  link_exports,
66
68
  open_exports,
67
69
  claim_exports,
@@ -3,7 +3,6 @@ import { getCliName } from "../utils/cli_name.js";
3
3
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
4
4
  import { servicesOption } from "../neon_services.js";
5
5
  import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES } from "../config_template.js";
6
- import { initChildEnv } from "../init/child.js";
7
6
  import { coerceAgentFlag } from "../utils/agent_flag.js";
8
7
  import { initPluginAgents, initSkillsMcpAgents } from "../init/plan.js";
9
8
  import { listSkillIds } from "../skills/catalog.js";
@@ -13,8 +12,7 @@ var init_exports = /* @__PURE__ */ __exportAll({
13
12
  builder: () => builder,
14
13
  command: () => command,
15
14
  describe: () => describe,
16
- handler: () => handler,
17
- initChildEnv: () => initChildEnv
15
+ handler: () => handler
18
16
  });
19
17
  const command = "init";
20
18
  const describe = "Set up coding agents and this directory for Neon. -y is Recommended. -y with Custom flags applies those answers without prompts.";
@@ -108,4 +106,4 @@ const handler = async (props) => {
108
106
  await runInit(props);
109
107
  };
110
108
  //#endregion
111
- export { builder, command, describe, handler, initChildEnv, init_exports as t };
109
+ export { builder, command, describe, handler, init_exports as t };
@@ -21,6 +21,7 @@ var link_exports = /* @__PURE__ */ __exportAll({
21
21
  command: () => command,
22
22
  describe: () => describe,
23
23
  handler: () => handler,
24
+ quoteFlagValue: () => quoteFlagValue,
24
25
  runLink: () => runLink,
25
26
  shouldOfferConfigInit: () => shouldOfferConfigInit
26
27
  });
@@ -887,4 +888,4 @@ const mustString = (value, name) => {
887
888
  return value;
888
889
  };
889
890
  //#endregion
890
- export { builder, command, describe, handler, runLink, shouldOfferConfigInit, link_exports as t };
891
+ export { builder, command, describe, handler, quoteFlagValue, runLink, shouldOfferConfigInit, link_exports as t };
@@ -8,15 +8,18 @@ import { writer } from "../writer.js";
8
8
  import { noPassthrough, single } from "../utils/flags.js";
9
9
  import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
10
10
  import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
11
- import { canPickAgentsInteractively } from "../utils/agent_picker.js";
12
11
  import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
12
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
13
13
  import { resolveMcpPlan } from "../mcp/plan.js";
14
14
  //#region src/commands/mcp.ts
15
15
  var mcp_exports = /* @__PURE__ */ __exportAll({
16
16
  builder: () => builder,
17
17
  command: () => "mcp",
18
18
  describe: () => describe,
19
- handler: () => handler
19
+ handler: () => handler,
20
+ mcpInstallError: () => mcpInstallError,
21
+ reportMcpInstall: () => reportMcpInstall,
22
+ setupNeonMcp: () => setupNeonMcp
20
23
  });
21
24
  const command = "mcp";
22
25
  const describe = "Install the Neon MCP server into coding agents";
@@ -87,20 +90,29 @@ const builder = (argv) => argv.usage("$0 mcp [options]").options({
87
90
  }
88
91
  }
89
92
  }).example("$0 mcp", "Interactive: config location, agents, then auth").example("$0 mcp -y", "Global config, installed apps else the host CLI agent, reuse or mint an API key").example("$0 mcp --oauth", "Install with OAuth; the agent signs in on first use").example("$0 mcp --agent cursor --agent claude-code", "Install into specific agents").example("$0 mcp --project", "Write project-level config").example("$0 mcp --read-only", "Hide write tools via ?readonly=true").example("$0 mcp --project-id <id>", "Pin tools to one project via ?projectId=").example("$0 mcp --category querying --category schema", "Limit tools to those categories").epilogue(helpEpilogue(`Installs ${NEON_MCP_URL}`, helpCsv("Supported agents at global scope", mcpGlobalAgents), helpCsv("--project does not support", mcpProjectDroppedAgents), helpCsv("Supported categories", NEON_MCP_CATEGORIES), "neon mcp -y:", " global config", " globally installed apps, else the host CLI agent", " reuse an existing Neon MCP API key, else mint an account-wide key", " write tools on, all categories", " no project pin (including from .neon)")).strict().check(noPassthrough("mcp"));
90
- const handler = async (props) => {
91
- const cwd = process.cwd();
92
- const interactive = canPickAgentsInteractively() && props.yes !== true;
93
- const linkedProjectId = readContextFile(props.contextFile).projectId;
93
+ /**
94
+ * Resolves auth, mints or reuses an API key, and writes the Neon MCP server into every
95
+ * resolved agent. No CLI-only concerns here (no `writer` table, no
96
+ * `recordCommandSuccessExtras`): a caller that only wants the outcome — `neon init` /
97
+ * `neon bootstrap` — gets it without rendering a table.
98
+ *
99
+ * Throws directly when zero agents succeed, since that path never reaches a results table
100
+ * worth showing (same as the CLI handler always did).
101
+ */
102
+ const setupNeonMcp = async (options) => {
103
+ const cwd = options.cwd ?? process.cwd();
104
+ const interactive = canPickAgentsInteractively() && options.yes !== true;
105
+ const linkedProjectId = readContextFile(options.contextFile).projectId;
94
106
  const plan = await resolveMcpPlan({
95
- project: props.project === true,
96
- oauth: props.oauth === true,
97
- agents: props.agent ?? [],
98
- yes: props.yes === true,
107
+ project: options.project === true,
108
+ oauth: options.oauth === true,
109
+ agents: options.agent ?? [],
110
+ yes: options.yes === true,
99
111
  cwd,
100
112
  interactive,
101
- readOnly: props.readOnly === true,
102
- projectId: props.projectId,
103
- categories: props.category ?? [],
113
+ readOnly: options.readOnly === true,
114
+ projectId: options.projectId,
115
+ categories: options.category ?? [],
104
116
  linkedProjectId
105
117
  });
106
118
  const { install, skipped } = resolveInstallTargets({
@@ -125,8 +137,8 @@ const handler = async (props) => {
125
137
  cwd
126
138
  }) : void 0;
127
139
  if (plan.auth === "api-key" && !existing) {
128
- if (!props.apiClient || !props.apiKey) throw new Error(`Authentication required. Run \`${getCliName()} auth\`, pass --api-key or use --oauth to install without a Neon credential.`);
129
- if (!canPickAgentsInteractively() && props.yes !== true && (props.agent ?? []).length === 0) throw new Error("No interactive terminal. Pass -y to mint into every detected agent, --agent <name> to name them or --oauth to install without minting.");
140
+ if (!options.apiClient || !options.apiKey) throw new Error(`Authentication required. Run \`${getCliName()} auth\`, pass --api-key or use --oauth to install without a Neon credential.`);
141
+ if (!canPickAgentsInteractively() && options.yes !== true && (options.agent ?? []).length === 0) throw new Error("No interactive terminal. Pass -y to mint into every detected agent, --agent <name> to name them or --oauth to install without minting.");
130
142
  }
131
143
  let auth;
132
144
  let minted;
@@ -140,7 +152,7 @@ const handler = async (props) => {
140
152
  if (plan.urlProjectId) log.warning("That key keeps its existing scope. Remove the Neon MCP entry to mint a project-scoped key, or pass --oauth to pin tools without a key.");
141
153
  } else {
142
154
  minted = await mintMcpApiKey({
143
- apiClient: props.apiClient,
155
+ apiClient: options.apiClient,
144
156
  projectId: plan.urlProjectId
145
157
  });
146
158
  auth = {
@@ -183,15 +195,28 @@ const handler = async (props) => {
183
195
  error: row.error
184
196
  });
185
197
  if (successes === 0) {
186
- if (minted && props.apiClient) {
187
- const withdrawn = await withdrawMintedKey(props.apiClient, minted);
198
+ if (minted && options.apiClient) {
199
+ const withdrawn = await withdrawMintedKey(options.apiClient, minted);
188
200
  throw new Error(withdrawn ? "Failed to write Neon MCP config to any agent. The minted API key has been revoked." : `Failed to write Neon MCP config to any agent. The minted API key could NOT be revoked. Remove it with \`${mintedKeyRevokeCommand(minted)}\`.`);
189
201
  }
190
202
  throw new Error("Failed to write Neon MCP config to any agent.");
191
203
  }
192
- log.info("URL: %s", url);
204
+ return {
205
+ scope: plan.scope,
206
+ rows,
207
+ failedAgents,
208
+ url,
209
+ auth: auth.kind,
210
+ ...minted ? { minted } : {}
211
+ };
212
+ };
213
+ /** The exact error `neon mcp` throws when some (not all) agents failed, or `undefined` on full success. */
214
+ const mcpInstallError = (outcome) => outcome.failedAgents.length > 0 ? /* @__PURE__ */ new Error(`Failed to write Neon MCP config for: ${outcome.failedAgents.join(", ")}.`) : void 0;
215
+ /** Writes the results table plus the URL/minted-key/OAuth notices. Shared by `neon mcp` and any in-process caller that wants the same human output. */
216
+ const reportMcpInstall = (props, outcome) => {
217
+ log.info("URL: %s", outcome.url);
193
218
  const out = writer(props);
194
- out.write(rows, {
219
+ out.write(outcome.rows, {
195
220
  fields: [
196
221
  "agent",
197
222
  "status",
@@ -200,10 +225,27 @@ const handler = async (props) => {
200
225
  title: "MCP"
201
226
  });
202
227
  out.end();
203
- if (minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", minted.name, minted.id, minted.projectId ? "project" : "account", mintedKeyRevokeCommand(minted));
204
- if (plan.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
205
- if (failedAgents.length > 0) throw new Error(`Failed to write Neon MCP config for: ${failedAgents.join(", ")}.`);
206
- recordCommandSuccessExtras({ scope: plan.scope });
228
+ if (outcome.minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", outcome.minted.name, outcome.minted.id, outcome.minted.projectId ? "project" : "account", mintedKeyRevokeCommand(outcome.minted));
229
+ if (outcome.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
230
+ };
231
+ const handler = async (props) => {
232
+ const outcome = await setupNeonMcp({
233
+ apiClient: props.apiClient,
234
+ apiKey: props.apiKey,
235
+ contextFile: props.contextFile,
236
+ cwd: props.cwd,
237
+ yes: props.yes,
238
+ oauth: props.oauth,
239
+ project: props.project,
240
+ agent: props.agent,
241
+ readOnly: props.readOnly,
242
+ projectId: props.projectId,
243
+ category: props.category
244
+ });
245
+ reportMcpInstall(props, outcome);
246
+ const error = mcpInstallError(outcome);
247
+ if (error) throw error;
248
+ recordCommandSuccessExtras({ scope: outcome.scope });
207
249
  };
208
250
  //#endregion
209
- export { builder, command, describe, handler, mcp_exports as t };
251
+ export { builder, command, describe, handler, mcpInstallError, reportMcpInstall, setupNeonMcp, mcp_exports as t };