neon 5.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,57 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
- import { log } from "../log.js";
3
- import { contextBranch, readContextFile } from "../context.js";
4
- import { recordCommandSuccessExtras } from "../analytics.js";
5
- import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
6
2
  import { getCliName } from "../utils/cli_name.js";
7
3
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
8
- import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
9
- import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE } from "../config_template.js";
10
- import { fetchTemplates } from "../init/bootstrap.js";
11
- import { initChildEnv, spawnCliChild } from "../init/child.js";
12
- import { agentSetupLabel, configSummaryLabel, formatInitDone, printInitBanner, printInitDone, shouldPrintInitBanner } from "../init/chrome.js";
13
- import { hasNeonConfigFile } from "./config.js";
14
- import { runAuthenticatedLink } from "../init/link.js";
4
+ import { servicesOption } from "../neon_services.js";
5
+ import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES } from "../config_template.js";
15
6
  import { coerceAgentFlag } from "../utils/agent_flag.js";
16
- import { INIT_NEEDS_YES_OR_TERMINAL, assertNamedAgentTooling, directoryIsEmpty, initPluginAgents, initSkillsMcpAgents, planExistingInit, resolveNamedAgents } from "../init/plan.js";
17
- import { canPickAgentsInteractively } from "../utils/agent_picker.js";
18
- import { pickInitConfigInteractively, pickInitLinkInteractively, pickInitTemplateInteractively } from "../init/wizard.js";
19
- import { runAgentTooling, runInitSteps } from "../init/tooling.js";
20
- import { handler as handler$1 } from "./bootstrap.js";
21
- import { INIT_CONFIG_SERVICES_CONFLICT, INIT_TEMPLATE_KEEPS_CONFIG, configPlanFromResolution, resolveInitConfigChoice, resolveInitTemplateChoice, shouldRefreshEnvAfterNewConfig } from "../init/choices.js";
22
- import { existsSync, readdirSync } from "node:fs";
23
- import { resolve } from "node:path";
7
+ import { initPluginAgents, initSkillsMcpAgents } from "../init/plan.js";
8
+ import { listSkillIds } from "../skills/catalog.js";
9
+ import { runInit } from "../init/run.js";
24
10
  //#region src/commands/init.ts
25
11
  var init_exports = /* @__PURE__ */ __exportAll({
26
12
  builder: () => builder,
27
13
  command: () => command,
28
14
  describe: () => describe,
29
- handler: () => handler,
30
- initChildEnv: () => initChildEnv
15
+ handler: () => handler
31
16
  });
32
17
  const command = "init";
33
- const describe = "Set up this directory for Neon: agent tooling, a linked project, and optionally neon.ts. In an empty directory, pick a template or skip scaffolding.";
18
+ const describe = "Set up coding agents and this directory for Neon. -y is Recommended. -y with Custom flags applies those answers without prompts.";
34
19
  const removedProtocol = () => `\`${getCliName()} init --data\` was removed. Run \`${getCliName()} init\` or \`${getCliName()} init -y\`.`;
35
20
  const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file", { hidden: true }).option("yes", {
36
21
  alias: "y",
37
22
  type: "boolean",
38
23
  default: false,
39
- describe: "Empty dir: scaffold the default template. --skip-template: plugin, or skills and MCP, for project folders, else the host CLI agent. Exits if none. Then link with defaults and create the bare neon.ts policy. If several organizations or projects exist, link prints IDs and exits"
40
- }).option("skip-template", {
24
+ describe: "Skip prompts. Alone: Recommended (detected agents, link when authenticated, default neon.ts). With --skill, MCP flags, --no-agent-setup, or --claimable: Custom using those flags"
25
+ }).option("agent-setup", {
26
+ type: "boolean",
27
+ default: true,
28
+ describe: "Install Neon into coding agents. Use --no-agent-setup to skip. Plugin vs skills is inferred from --skill and MCP flags"
29
+ }).option("claimable", {
41
30
  type: "boolean",
42
31
  default: false,
43
- describe: "Do not scaffold a template. Set up agents, link a project, and optionally neon.ts in this directory"
44
- }).option("template", {
32
+ describe: "Create a claimable project that expires in 72 hours unless claimed. Selects Custom"
33
+ }).option("package-manager", {
45
34
  type: "string",
46
- describe: "Template to scaffold into an empty directory. Conflicts with --skip-template"
35
+ choices: [
36
+ "npm",
37
+ "pnpm",
38
+ "yarn",
39
+ "bun"
40
+ ],
41
+ describe: "Package manager for neon.ts dependencies. -y uses this without asking. Custom asks only when none is detected"
47
42
  }).option("link", {
48
43
  type: "boolean",
49
44
  default: true,
50
45
  describe: "Link a Neon project during setup. Use --no-link to skip without being asked"
51
46
  }).option("config", {
52
47
  type: "boolean",
53
- describe: "Existing app or --skip-template: create neon.ts after linking. Use --no-config to skip. Omitted in a terminal: you will be asked. Scaffolding a template keeps that template's neon.ts"
48
+ describe: "Create neon.ts after linking. Use --no-config to skip. Omitted in Custom: you will be asked"
54
49
  }).option("services", servicesOption({
55
50
  key: "services",
56
51
  allowed: CONFIG_INIT_SERVICES,
57
- describe: "Existing app or --skip-template: services to declare in neon.ts",
52
+ describe: "Services to declare in neon.ts",
58
53
  noneMeans: CONFIG_INIT_NONE_MEANS,
59
- also: "Implies creating neon.ts. Cannot be combined with --no-config. Ignored when scaffolding a template."
54
+ also: "Implies creating neon.ts. Cannot be combined with --no-config."
60
55
  })).option("org-id", {
61
56
  describe: "Forwarded to link: organization ID to link to",
62
57
  type: "string"
@@ -77,8 +72,24 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
77
72
  alias: "a",
78
73
  type: "array",
79
74
  string: true,
80
- describe: "Coding agent to install into (repeatable). Forwarded to plugins, or to skills and mcp. Skips agent selection. Values listed below",
75
+ describe: "Coding agent to install into (repeatable). Skips agent selection. Values listed below",
81
76
  coerce: coerceAgentFlag
77
+ }).option("skill", {
78
+ type: "array",
79
+ string: true,
80
+ describe: "Neon skill to install (repeatable). Selects skills setup (not the plugin) and skips the skills picker. With MCP flags, also configures MCP. Values listed below"
81
+ }).option("mcp-config-location", {
82
+ type: "string",
83
+ choices: ["global", "project"],
84
+ describe: "Where to configure the Neon MCP server: global (user config, same as neon mcp) or project (this directory, same as neon mcp --project). Selects skills and MCP setup"
85
+ }).option("mcp-auth", {
86
+ type: "string",
87
+ choices: ["oauth", "api-key"],
88
+ describe: "MCP authentication. Selects skills and MCP setup. oauth is neon mcp --oauth"
89
+ }).option("mcp-project-scoped", {
90
+ type: "boolean",
91
+ default: false,
92
+ describe: "Limit MCP tools to the linked project, same as neon mcp --project-id. Selects skills and MCP setup"
82
93
  }).option("data", {
83
94
  hidden: true,
84
95
  type: "string"
@@ -86,281 +97,13 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
86
97
  alias: "o",
87
98
  hidden: true,
88
99
  describe: "Not supported; the commands init runs print their own output"
89
- }).example("$0 init", "Set up this directory (template picker if empty)").example("$0 init --skip-template", "Empty dir: agents, link, and neon.ts — no template files").example("$0 init -y", "Default template, or existing-app defaults").example("$0 init --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("Empty directory: pick a starter template, or skip scaffolding and only set up agents, a Neon project, and neon.ts. That skip is not on `neon bootstrap`.", "Interactive agent setup: plugin (recommended), skills and MCP separately, or skip agent setup. Never both plugin and skills+MCP.", "neon.ts is optional when you skip the template or set up an existing app. Saying no skips the services picker and does not write the file. Scaffolding a template keeps that template's neon.ts.", "-y installs the plugin when Cursor, Claude Code, or Codex is in project folders, else the host CLI agent. Otherwise skills and MCP. If none are found, it exits: pass --agent <name>, run from a supported agent, or omit -y in a terminal to pick. Then link unless already linked. If several organizations or projects exist, link prints IDs and exits.", "--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with -y.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).check((argv) => {
100
+ }).example("$0 init", "Recommended or Custom setup").example("$0 init -y", "Recommended setup without prompts").example("$0 init -y --skill neon --mcp-auth oauth --mcp-config-location project", "Custom: those skills and MCP, Recommended defaults for the rest").example("$0 init --no-agent-setup --claimable", "Skip agent setup and create a claimable project").example("$0 init --agent cursor --agent claude-code", "Skip agent selection; install tooling for those agents").epilogue(helpEpilogue("Recommended setup installs tooling for detected coding agents, links a Neon project when the CLI is authenticated or the session is interactive, and writes a default neon.ts.", "init sets up the current directory in place. It does not scaffold starter files.", "-y alone is Recommended. -y with --skill, MCP flags, --no-agent-setup, or --claimable is Custom: those flags, Recommended defaults for unanswered questions. -y never opens a browser.", "--skill selects skills (not the plugin). MCP flags select skills and MCP. --no-agent-setup skips agent setup. --agent without those flags installs the plugin (and skills/MCP for agents the plugin cannot cover).", "Without a TTY, pass -y or enough flags to answer every question.", "--agent / -a is forwarded to plugins, or to skills and mcp. It skips agent selection, including with -y.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()), helpCsv("Skills", listSkillIds()))).check((argv) => {
90
101
  if (argv.data !== void 0) throw new Error(removedProtocol());
91
102
  if (argv.help !== true && (argv.output === "json" || argv.output === "yaml")) throw new Error(`\`${getCliName()} init\` does not support --output. The commands it runs print their own output.`);
92
103
  return true;
93
104
  }).strict();
94
- const isLinked = (contextFile) => {
95
- const projectId = readContextFile(contextFile).projectId;
96
- return typeof projectId === "string" && projectId.length > 0;
97
- };
98
- const nestedBootstrapDirectory = (cwd) => resolve(cwd) === resolve(process.cwd()) ? "." : cwd;
99
- const parsedInitServices = (raw) => {
100
- const values = servicesFlagValue(raw);
101
- if (values === void 0) return;
102
- return parseServices(values, {
103
- allowed: CONFIG_INIT_SERVICES,
104
- whyUnavailable: CONFIG_INIT_UNAVAILABLE,
105
- flag: "--services",
106
- noneMeans: CONFIG_INIT_NONE_MEANS,
107
- onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
108
- });
109
- };
110
- const nestedBootstrapProps = (props, cwd, contextFile, template, linkInputs) => ({
111
- apiClient: props.apiClient,
112
- apiKey: props.apiKey,
113
- apiHost: props.apiHost,
114
- output: props.output,
115
- contextFile,
116
- directory: nestedBootstrapDirectory(cwd),
117
- force: false,
118
- listTemplates: false,
119
- default: template.useDefault,
120
- install: true,
121
- git: true,
122
- link: props.link !== false,
123
- printBanner: false,
124
- skipDoneSummary: true,
125
- linkNoConfig: true,
126
- narrate: "human",
127
- ...template.selected ? { selectedTemplate: template.selected } : {},
128
- ...!template.selected && template.id !== void 0 ? { template: template.id } : {},
129
- linkInputs,
130
- ...props.agent !== void 0 ? { agent: props.agent } : {},
131
- ...props.configDir ? { configDir: props.configDir } : {},
132
- ...props.profile ? { profile: props.profile } : {},
133
- ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
134
- ...props.clientId ? { clientId: props.clientId } : {},
135
- ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
136
- ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {},
137
- ...props.analytics === false ? { analytics: false } : {},
138
- ...props.run ? { run: props.run } : {},
139
- ...props.linkProject ? { linkProject: props.linkProject } : {},
140
- ...props.pickLink ? { pickLink: props.pickLink } : {},
141
- ...props.pickAgentSetup ? { pickAgentSetup: props.pickAgentSetup } : {},
142
- ...props.detectProjectAgents ? { detectProjectAgents: props.detectProjectAgents } : {},
143
- ...props.detectAgent ? { detectAgent: props.detectAgent } : {},
144
- ...props.hasProjectPlugins ? { hasProjectPlugins: props.hasProjectPlugins } : {}
145
- });
146
- const projectRow = (input) => {
147
- if (input.alreadyLinked) return "already linked";
148
- if (input.linkedNow) return "linked";
149
- return "not linked";
150
- };
151
- const configSummary = (input) => {
152
- if (input.plan.kind === "skip") return "skipped";
153
- if (input.existingConfig) return "existing";
154
- return "created";
155
- };
156
- const printNestedBootstrapDone = (result, cwd, fallbackTitle) => {
157
- const hasConfig = result?.hasNeonConfig === true || hasNeonConfigFile(cwd);
158
- printInitDone(formatInitDone({
159
- heading: "Neon setup complete.",
160
- rows: [
161
- {
162
- label: "Template",
163
- value: result?.templateTitle ?? fallbackTitle
164
- },
165
- {
166
- label: "Agents",
167
- value: agentSetupLabel(result?.agentSetup ?? "skip")
168
- },
169
- {
170
- label: "Project",
171
- value: projectRow({
172
- alreadyLinked: false,
173
- linkedNow: result?.linked === true
174
- })
175
- },
176
- {
177
- label: "Config",
178
- value: configSummaryLabel(hasConfig ? "template" : "skipped")
179
- }
180
- ],
181
- next: []
182
- }));
183
- };
184
- const noteTemplateKeepsShippedConfig = (config, services) => {
185
- if (config === void 0 && services === void 0) return;
186
- log.warning(INIT_TEMPLATE_KEEPS_CONFIG);
187
- };
188
- const recordInitSuccess = (kind, agentSetup) => {
189
- recordCommandSuccessExtras({
190
- init_kind: kind,
191
- ...agentSetup !== void 0 ? { agent_setup: agentSetup } : {}
192
- });
193
- };
194
105
  const handler = async (props) => {
195
- if (props.output === "json" || props.output === "yaml") throw new Error(`\`${getCliName()} init\` does not support --output. The commands it runs print their own output.`);
196
- const cwd = props.cwd ?? process.cwd();
197
- const names = existsSync(cwd) ? readdirSync(cwd) : [];
198
- const contextFile = resolve(cwd, props.contextFile);
199
- const yes = props.yes === true;
200
- const named = resolveNamedAgents(props.agent ?? []);
201
- assertNamedAgentTooling(named, "init", yes ? { yes: true } : void 0);
202
- const run = props.run ?? spawnCliChild;
203
- const explicitKey = props.profile ? "" : credentialInputs().apiKeyFlag;
204
- const authEnv = explicitKey ? { NEON_API_KEY: explicitKey } : void 0;
205
- const forward = {
206
- ...props.configDir ? { configDir: props.configDir } : {},
207
- ...props.profile ? { profile: props.profile } : {},
208
- apiHost: props.apiHost,
209
- contextFile,
210
- ...props.analytics === false ? { analytics: false } : {}
211
- };
212
- const services = parsedInitServices(props.services);
213
- if (props.config === false && services !== void 0) throw new Error(INIT_CONFIG_SERVICES_CONFLICT);
214
- const linkInputs = {
215
- ...props.orgId ? { orgId: props.orgId } : {},
216
- ...props.projectId ? { projectId: props.projectId } : {},
217
- ...props.projectName ? { projectName: props.projectName } : {},
218
- ...props.regionId ? { regionId: props.regionId } : {},
219
- ...props.branch ? { branch: props.branch } : {}
220
- };
221
- const hasExplicitLinkInputs = props.orgId !== void 0 || props.projectId !== void 0 || props.projectName !== void 0 || props.regionId !== void 0 || props.branch !== void 0;
222
- const templateChoice = resolveInitTemplateChoice({
223
- empty: directoryIsEmpty(names),
224
- yes,
225
- skipTemplate: props.skipTemplate === true,
226
- template: props.template
227
- });
228
- if (shouldPrintInitBanner(yes)) printInitBanner();
229
- if (templateChoice.kind === "default" || templateChoice.kind === "template") {
230
- if (templateChoice.kind === "template" && !yes && props.runBootstrap === void 0 && !canPickAgentsInteractively()) throw new Error(INIT_NEEDS_YES_OR_TERMINAL);
231
- const runBootstrap = props.runBootstrap ?? handler$1;
232
- noteTemplateKeepsShippedConfig(props.config, services);
233
- const result = await runBootstrap(nestedBootstrapProps(props, cwd, contextFile, {
234
- useDefault: yes,
235
- ...templateChoice.kind === "template" ? { id: templateChoice.id } : {}
236
- }, linkInputs));
237
- printNestedBootstrapDone(result, cwd, templateChoice.kind === "template" ? templateChoice.id : "default");
238
- recordInitSuccess("empty-template", result?.agentSetup);
239
- return;
240
- }
241
- if (templateChoice.kind === "ask") {
242
- if (props.pickTemplate === void 0 && !canPickAgentsInteractively()) throw new Error(INIT_NEEDS_YES_OR_TERMINAL);
243
- const templates = await (props.fetchTemplates ?? fetchTemplates)();
244
- const picked = await (props.pickTemplate ?? pickInitTemplateInteractively)(templates);
245
- if (picked.kind === "template") {
246
- const runBootstrap = props.runBootstrap ?? handler$1;
247
- noteTemplateKeepsShippedConfig(props.config, services);
248
- const result = await runBootstrap(nestedBootstrapProps(props, cwd, contextFile, {
249
- selected: picked.template,
250
- useDefault: false
251
- }, linkInputs));
252
- printNestedBootstrapDone(result, cwd, picked.template.id);
253
- recordInitSuccess("empty-template", result?.agentSetup);
254
- return;
255
- }
256
- }
257
- const alreadyLinked = isLinked(contextFile);
258
- const shouldLink = props.link !== false && (!alreadyLinked || hasExplicitLinkInputs);
259
- const existingConfig = hasNeonConfigFile(cwd);
260
- const canAskConfig = props.pickConfig !== void 0 || canPickAgentsInteractively();
261
- const agentSetup = await runAgentTooling({
262
- cwd,
263
- yes,
264
- run,
265
- forward,
266
- authEnv,
267
- narrate: "human",
268
- command: "init",
269
- ...named.length > 0 ? { agents: named } : {},
270
- ...props.pickAgentSetup ? { pickAgentSetup: props.pickAgentSetup } : {},
271
- ...props.detectProjectAgents ? { detectProjectAgents: props.detectProjectAgents } : {},
272
- ...props.detectAgent ? { detectAgent: props.detectAgent } : {},
273
- ...props.hasProjectPlugins ? { hasProjectPlugins: props.hasProjectPlugins } : {}
274
- });
275
- const canAskLink = props.pickLink !== void 0 || canPickAgentsInteractively();
276
- const acceptedLink = shouldLink && (yes || !canAskLink || await (props.pickLink ?? pickInitLinkInteractively)());
277
- if (acceptedLink) await (props.linkProject ?? runAuthenticatedLink)({
278
- apiClient: props.apiClient,
279
- apiKey: props.apiKey,
280
- apiHost: props.apiHost,
281
- output: props.output,
282
- contextFile,
283
- yes,
284
- clear: false,
285
- checks: true,
286
- envPull: true,
287
- config: false,
288
- cwd,
289
- ...linkInputs,
290
- ...props.configDir ? { configDir: props.configDir } : {},
291
- ...props.profile ? { profile: props.profile } : {},
292
- ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
293
- ...props.clientId ? { clientId: props.clientId } : {},
294
- ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
295
- ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {}
296
- });
297
- const configResolution = resolveInitConfigChoice({
298
- flag: props.config,
299
- yes,
300
- canAsk: canAskConfig,
301
- existingConfig,
302
- ...services !== void 0 ? { services } : {}
303
- });
304
- const accepted = configResolution.kind === "ask" ? await (props.pickConfig ?? pickInitConfigInteractively)() : void 0;
305
- const configPlan = configPlanFromResolution(configResolution, accepted);
306
- if (configPlan.kind === "write") {
307
- await runInitSteps(planExistingInit({
308
- linked: true,
309
- yes,
310
- agentSetup: "skip",
311
- config: configPlan
312
- }), {
313
- cwd,
314
- run,
315
- forward,
316
- authEnv,
317
- narrate: "human"
318
- });
319
- const context = readContextFile(contextFile);
320
- const branch = contextBranch(context);
321
- if (shouldRefreshEnvAfterNewConfig({
322
- wroteNewFile: !existingConfig,
323
- projectId: context.projectId,
324
- branch
325
- })) try {
326
- await runInitSteps([["env", "pull"]], {
327
- cwd,
328
- run,
329
- forward,
330
- authEnv,
331
- narrate: "human"
332
- });
333
- } catch (err) {
334
- const message = err instanceof Error ? err.message : String(err);
335
- log.warning(`Created neon.ts, but pulling its Neon env vars failed: %s
336
- Run \`${getCliName()} env pull\` once resolved.`, message);
337
- }
338
- }
339
- printInitDone(formatInitDone({
340
- heading: "Neon setup complete.",
341
- rows: [
342
- {
343
- label: "Agents",
344
- value: agentSetupLabel(agentSetup)
345
- },
346
- {
347
- label: "Project",
348
- value: projectRow({
349
- alreadyLinked,
350
- linkedNow: acceptedLink
351
- })
352
- },
353
- {
354
- label: "Config",
355
- value: configSummaryLabel(configSummary({
356
- plan: configPlan,
357
- existingConfig
358
- }))
359
- }
360
- ],
361
- next: []
362
- }));
363
- recordInitSuccess(templateChoice.kind === "existing" ? "existing" : "empty-skip", agentSetup);
106
+ await runInit(props);
364
107
  };
365
108
  //#endregion
366
- export { builder, command, describe, handler, initChildEnv, init_exports as t };
109
+ export { builder, command, describe, handler, init_exports as t };
@@ -8,16 +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";
13
- import { confirmMcpInstall } from "../mcp/wizard.js";
12
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
14
13
  import { resolveMcpPlan } from "../mcp/plan.js";
15
14
  //#region src/commands/mcp.ts
16
15
  var mcp_exports = /* @__PURE__ */ __exportAll({
17
16
  builder: () => builder,
18
17
  command: () => "mcp",
19
18
  describe: () => describe,
20
- handler: () => handler
19
+ handler: () => handler,
20
+ mcpInstallError: () => mcpInstallError,
21
+ reportMcpInstall: () => reportMcpInstall,
22
+ setupNeonMcp: () => setupNeonMcp
21
23
  });
22
24
  const command = "mcp";
23
25
  const describe = "Install the Neon MCP server into coding agents";
@@ -87,21 +89,30 @@ const builder = (argv) => argv.usage("$0 mcp [options]").options({
87
89
  return parseMcpCategories(parts);
88
90
  }
89
91
  }
90
- }).example("$0 mcp", "Interactive: config location, agents, auth, then confirm").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"));
91
- const handler = async (props) => {
92
- const cwd = process.cwd();
93
- const interactive = canPickAgentsInteractively() && props.yes !== true;
94
- const linkedProjectId = readContextFile(props.contextFile).projectId;
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"));
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;
95
106
  const plan = await resolveMcpPlan({
96
- project: props.project === true,
97
- oauth: props.oauth === true,
98
- agents: props.agent ?? [],
99
- yes: props.yes === true,
107
+ project: options.project === true,
108
+ oauth: options.oauth === true,
109
+ agents: options.agent ?? [],
110
+ yes: options.yes === true,
100
111
  cwd,
101
112
  interactive,
102
- readOnly: props.readOnly === true,
103
- projectId: props.projectId,
104
- categories: props.category ?? [],
113
+ readOnly: options.readOnly === true,
114
+ projectId: options.projectId,
115
+ categories: options.category ?? [],
105
116
  linkedProjectId
106
117
  });
107
118
  const { install, skipped } = resolveInstallTargets({
@@ -126,22 +137,8 @@ const handler = async (props) => {
126
137
  cwd
127
138
  }) : void 0;
128
139
  if (plan.auth === "api-key" && !existing) {
129
- 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.`);
130
- 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.");
131
- }
132
- if (interactive) {
133
- if (!await confirmMcpInstall({
134
- scope: plan.scope,
135
- install,
136
- skipped,
137
- auth: plan.auth,
138
- reuse: existing !== void 0,
139
- url,
140
- mintProjectId: plan.urlProjectId
141
- })) {
142
- log.info("Aborted. Nothing was written.");
143
- return;
144
- }
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.");
145
142
  }
146
143
  let auth;
147
144
  let minted;
@@ -155,7 +152,7 @@ const handler = async (props) => {
155
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.");
156
153
  } else {
157
154
  minted = await mintMcpApiKey({
158
- apiClient: props.apiClient,
155
+ apiClient: options.apiClient,
159
156
  projectId: plan.urlProjectId
160
157
  });
161
158
  auth = {
@@ -198,15 +195,28 @@ const handler = async (props) => {
198
195
  error: row.error
199
196
  });
200
197
  if (successes === 0) {
201
- if (minted && props.apiClient) {
202
- const withdrawn = await withdrawMintedKey(props.apiClient, minted);
198
+ if (minted && options.apiClient) {
199
+ const withdrawn = await withdrawMintedKey(options.apiClient, minted);
203
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)}\`.`);
204
201
  }
205
202
  throw new Error("Failed to write Neon MCP config to any agent.");
206
203
  }
207
- 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);
208
218
  const out = writer(props);
209
- out.write(rows, {
219
+ out.write(outcome.rows, {
210
220
  fields: [
211
221
  "agent",
212
222
  "status",
@@ -215,10 +225,27 @@ const handler = async (props) => {
215
225
  title: "MCP"
216
226
  });
217
227
  out.end();
218
- if (minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", minted.name, minted.id, minted.projectId ? "project" : "account", mintedKeyRevokeCommand(minted));
219
- if (plan.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
220
- if (failedAgents.length > 0) throw new Error(`Failed to write Neon MCP config for: ${failedAgents.join(", ")}.`);
221
- 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 });
222
249
  };
223
250
  //#endregion
224
- export { builder, command, describe, handler, mcp_exports as t };
251
+ export { builder, command, describe, handler, mcpInstallError, reportMcpInstall, setupNeonMcp, mcp_exports as t };