neon 2.47.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +395 -0
  5. package/dist/_shared/env-core/reuse-secrets.js +179 -0
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +575 -658
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +642 -681
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -341
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -64
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +133 -147
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +9 -7
@@ -1,729 +1,572 @@
1
- import { writeFileSync } from "node:fs";
2
- import { unlink } from "node:fs/promises";
3
- import { resolve } from "node:path";
4
- import { execa } from "execa";
1
+ import { FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate } from "../bootstrap.js";
2
+ import { detectIde, isCursorInstalled, isVSCodeInstalled } from "../detect_agent.js";
5
3
  import { resolveAddMcpAgentId } from "../agents.js";
6
- import { FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, } from "../bootstrap.js";
7
- import { detectIde, isCursorInstalled, isVSCodeInstalled, } from "../detect_agent.js";
4
+ import { NEON_EXTENSION_ID, downloadVsix } from "../vsix.js";
8
5
  import { findEditorCommand } from "../extension.js";
9
6
  import { inspectProject } from "../inspect.js";
10
7
  import { ensureNeonctl } from "../neonctl.js";
11
8
  import { ensureSkillsUpToDate } from "../skills.js";
12
- import { downloadVsix, NEON_EXTENSION_ID } from "../vsix.js";
9
+ import { writeFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { unlink } from "node:fs/promises";
12
+ import { execa } from "execa";
13
+ //#region src/init/phases/setup.ts
13
14
  /**
14
- * Comprehensive setup phase: inspects repo state, collects user preferences,
15
- * then batches all installation commands together.
16
- *
17
- * With --data JSON, the agent sends inspection results AND user preferences
18
- * in a single call, so the CLI can go straight to installation.
19
- */
20
- export async function handleSetupPhase(options) {
21
- // Parse features from comma-separated string (e.g. "database,auth" from agent --data)
22
- if (typeof options.features === "string") {
23
- options.features = options.features
24
- .split(",")
25
- .map((f) => f.trim());
26
- }
27
- // Treat "none" as no template selected
28
- const templateWasAnswered = options.template !== undefined;
29
- if (options.template === "none") {
30
- options.template = undefined;
31
- }
32
- // Resolve template requirements if a template was selected but requires not yet populated
33
- if (options.template && !options.templateRequires) {
34
- const templates = await fetchTemplates();
35
- const selected = templates.find((t) => t.id === options.template);
36
- if (selected) {
37
- options.templateRequires = selected.requires;
38
- }
39
- }
40
- // --execute: run the batched installation (legacy path)
41
- if (options.execute) {
42
- return executeBatchedInstallation(await mergeCliInspection(options));
43
- }
44
- // Treat any explicit mode value that isn't "customize"/"custom" as defaults.
45
- // Also treat as defaults when mode is missing but agent reported back
46
- // (template was answered, nothing left to customize).
47
- const hasReportedBack = options.mode || templateWasAnswered;
48
- if (hasReportedBack &&
49
- options.mode !== "customize" &&
50
- options.mode !== "custom") {
51
- const merged = await mergeCliInspection(options);
52
- return executeBatchedInstallation({
53
- ...merged,
54
- mcpScope: merged.mcpScope ?? "global",
55
- skillsScope: merged.skillsScope ?? "project",
56
- installExtension: false,
57
- });
58
- }
59
- // User chose "customize" (also accept "custom" — agents sometimes truncate)
60
- if (options.mode === "customize" || options.mode === "custom") {
61
- const merged = await mergeCliInspection(options);
62
- const shouldInstallExt = merged.installExtension ?? isVscodeBasedIde(merged);
63
- return executeBatchedInstallation({
64
- ...merged,
65
- mcpScope: merged.mcpScope ?? "global",
66
- skillsScope: merged.skillsScope ?? "project",
67
- installExtension: shouldInstallExt,
68
- });
69
- }
70
- // Default: send inspection checks with user preferences (all in one response)
71
- return buildBulkInspection(options);
15
+ * Comprehensive setup phase: inspects repo state, collects user preferences,
16
+ * then batches all installation commands together.
17
+ *
18
+ * With --data JSON, the agent sends inspection results AND user preferences
19
+ * in a single call, so the CLI can go straight to installation.
20
+ */
21
+ async function handleSetupPhase(options) {
22
+ if (typeof options.features === "string") options.features = options.features.split(",").map((f) => f.trim());
23
+ const templateWasAnswered = options.template !== void 0;
24
+ if (options.template === "none") options.template = void 0;
25
+ if (options.template && !options.templateRequires) {
26
+ const selected = (await fetchTemplates()).find((t) => t.id === options.template);
27
+ if (selected) options.templateRequires = selected.requires;
28
+ }
29
+ if (options.execute) return executeBatchedInstallation(await mergeCliInspection(options));
30
+ if ((options.mode || templateWasAnswered) && options.mode !== "customize" && options.mode !== "custom") {
31
+ const merged = await mergeCliInspection(options);
32
+ return executeBatchedInstallation({
33
+ ...merged,
34
+ mcpScope: merged.mcpScope ?? "global",
35
+ skillsScope: merged.skillsScope ?? "project",
36
+ installExtension: false
37
+ });
38
+ }
39
+ if (options.mode === "customize" || options.mode === "custom") {
40
+ const merged = await mergeCliInspection(options);
41
+ const shouldInstallExt = merged.installExtension ?? isVscodeBasedIde(merged);
42
+ return executeBatchedInstallation({
43
+ ...merged,
44
+ mcpScope: merged.mcpScope ?? "global",
45
+ skillsScope: merged.skillsScope ?? "project",
46
+ installExtension: shouldInstallExt
47
+ });
48
+ }
49
+ return buildBulkInspection(options);
72
50
  }
73
51
  function buildTemplatePreference(templates) {
74
- return [
75
- {
76
- id: "template",
77
- question: "No application was detected in this directory. Would you like to scaffold a new project from a template?",
78
- phase: "before_checks",
79
- options: [
80
- ...templates.map((t) => {
81
- const tools = t.tools && t.tools.length > 0
82
- ? ` (${t.tools.join(", ")})`
83
- : "";
84
- return {
85
- value: t.id,
86
- label: `${t.title}${tools} — ${t.description}`,
87
- };
88
- }),
89
- {
90
- value: "none",
91
- label: "No thanks — continue without scaffolding",
92
- },
93
- ],
94
- default: "none",
95
- },
96
- ];
52
+ return [{
53
+ id: "template",
54
+ question: "No application was detected in this directory. Would you like to scaffold a new project from a template?",
55
+ phase: "before_checks",
56
+ options: [...templates.map((t) => {
57
+ const tools = t.tools && t.tools.length > 0 ? ` (${t.tools.join(", ")})` : "";
58
+ return {
59
+ value: t.id,
60
+ label: `${t.title}${tools} — ${t.description}`
61
+ };
62
+ }), {
63
+ value: "none",
64
+ label: "No thanks — continue without scaffolding"
65
+ }],
66
+ default: "none"
67
+ }];
97
68
  }
98
69
  async function buildBulkInspection(options) {
99
- const hasApp = options.hasApp !== false;
100
- const detectedIde = detectIde();
101
- // If no IDE detected (e.g. standalone terminal), check what's installed
102
- const installedEditors = [];
103
- if (!detectedIde) {
104
- if (isCursorInstalled())
105
- installedEditors.push("cursor");
106
- if (isVSCodeInstalled())
107
- installedEditors.push("vscode");
108
- }
109
- // Fetch available templates when no app is detected
110
- let templatePreferences = [];
111
- if (!hasApp) {
112
- let templates = FALLBACK_TEMPLATES;
113
- try {
114
- const fetched = await fetchTemplates();
115
- if (fetched && fetched.length > 0)
116
- templates = fetched;
117
- }
118
- catch { }
119
- templatePreferences = buildTemplatePreference(templates);
120
- }
121
- return {
122
- phase: "setup",
123
- status: hasApp ? "pending" : "bootstrap_needed",
124
- detectedIde: detectedIde?.toLowerCase() ?? null,
125
- installedEditors: installedEditors.length > 0 ? installedEditors : null,
126
- // Pre-detected state from filesystem inspection
127
- mcpConfigured: options.mcpConfigured ?? null,
128
- mcpScope: options.mcpScope ?? null,
129
- skillsInstalled: options.skillsInstalled ?? null,
130
- skillsScope: options.skillsScope ?? null,
131
- nextAction: {
132
- type: "agent_check",
133
- instructions: [
134
- "IMPORTANT: Do NOT summarize this response or ask the user for consent/confirmation before starting. Proceed IMMEDIATELY with the first userPreference question.",
135
- "",
136
- hasApp
137
- ? "Perform the agent checks listed above (MCP server status and your agent identity), then present each userPreference question to the user ONE AT A TIME, in order. Wait for the user's answer before showing the next question. Respect the `condition` field — only show a question if its condition is met."
138
- : "No application was detected in this directory. Ask the user if they'd like to scaffold a new project from a template (the `template` preference). Present ALL template options and the 'No thanks' option — do NOT auto-select even if there is only one template. If the user selects a template, the scaffolded template includes agent skills so skills installation will be skipped. If the user chooses 'none', continue with the remaining setup preferences normally. Then perform the agent checks and present the remaining preferences ONE AT A TIME.",
139
- "",
140
- `The CLI has pre-detected the following from the filesystem: MCP server: ${options.mcpConfigured ? `configured (${options.mcpScope})` : "not configured"}. Agent skills: ${options.skillsInstalled ? `installed (${options.skillsScope})` : String(options.skillsScope ?? "").includes("partial") ? `partially installed (${options.skillsScope}) — missing skills will be auto-installed to the same scope` : "not installed"}. Report these findings to the user before asking preferences. Only ask about scope/options for components that are NOT already configured. Do NOT ask about skills scope if skills are partially installed — they will be completed automatically.`,
141
- "",
142
- "IMPORTANT (Cursor users): Cursor disables project-level MCP servers by default as a security measure. If the user is in Cursor and chooses project-level MCP scope, warn them that they will need to manually enable the Neon server in Cursor Settings > MCP after installation. Recommend global scope for Cursor to avoid this extra step.",
143
- "",
144
- "GROUPING: Preferences that share the same `group` field should be presented together in a single message (e.g. list all customize options at once and let the user answer them together). Preferences without a `group` must be asked individually.",
145
- "",
146
- detectedIde
147
- ? `The CLI has detected the IDE as: ${detectedIde.toLowerCase()}. Include this as the "ide" field in your reportBack data. IMPORTANT: The IDE and the agent are different — you may be Claude Code (agent) running inside Cursor (IDE). The extension installs into the IDE, so if the IDE is Cursor/VS Code/Windsurf, the extension IS applicable even if you are Claude Code.`
148
- : installedEditors.length > 0
149
- ? `No IDE detected, but the following editors are installed: ${installedEditors.join(", ")}. The "installedEditors" field in this response lists them. If the user wants the extension installed, ask which editor to install it for and include that as the "ide" field in your reportBack data. If not, set "ide" to "none".`
150
- : `No IDE or supported editors detected. Set "ide" to "none" in your reportBack data.`,
151
- "",
152
- "After all questions are answered, call reportBack with a single --data JSON containing: agent, ide, mcpConfigured, and all preference answers. The CLI will inspect the project and merge results automatically.",
153
- ].join("\n"),
154
- checks: [
155
- {
156
- id: "neonctl",
157
- description: "The Neon CLI will be installed or updated automatically (no action needed from the agent)",
158
- lookFor: [],
159
- },
160
- {
161
- id: "mcp_server",
162
- description: "Check if the Neon MCP server is already configured in your MCP server list",
163
- lookFor: [
164
- "An MCP server entry named 'Neon' or with URL containing 'mcp.neon.tech'",
165
- ],
166
- },
167
- {
168
- id: "agent_type",
169
- description: "Identify which coding agent is running this command",
170
- lookFor: [
171
- "Determine which agent you are: cursor, claude-code, copilot, vscode, windsurf, codex, cline, gemini-cli, goose, opencode, or antigravity",
172
- "Report your own agent identifier — this is used to configure the MCP server for the correct tool",
173
- ],
174
- },
175
- ...(detectedIde
176
- ? [
177
- {
178
- id: "extension_installed",
179
- description: "Check if the Neon editor extension (databricks.neon-local-connect) is already installed in the IDE (NOT the agent — e.g. if you are Claude Code running inside Cursor, check Cursor's extensions)",
180
- lookFor: [
181
- "Run the IDE's --list-extensions command or check installed extensions for 'databricks.neon-local-connect' or 'Neon Local Connect'",
182
- "If the extension is found, set installExtension to false in your reportBack data and SKIP the installExtension question",
183
- ],
184
- },
185
- ]
186
- : []),
187
- ],
188
- userPreferences: [
189
- ...templatePreferences,
190
- // For brownfield flows, ask which Neon features to enable
191
- ...(hasApp
192
- ? [
193
- {
194
- id: "features",
195
- question: "Which Neon features would you like to enable for this project?",
196
- phase: "after_checks",
197
- options: [
198
- {
199
- value: "database",
200
- label: "Database (always included)",
201
- },
202
- {
203
- value: "database,auth",
204
- label: "Database + Neon Auth (adds authentication via Neon)",
205
- },
206
- ],
207
- default: "database",
208
- context: "Database connectivity is always set up. Neon Auth adds user authentication powered by Neon. More features (Functions, AI Gateway, Object Storage) will be available soon.",
209
- },
210
- ]
211
- : []),
212
- // Only show defaults/customize when there's something to customize:
213
- // MCP not configured, skills need scope choice, or extension not detected.
214
- ...(() => {
215
- const isPartialSkills = String(options.skillsScope ?? "").includes("partial");
216
- const needsMcpChoice = !options.mcpConfigured;
217
- const needsSkillsChoice = !options.skillsInstalled && !isPartialSkills;
218
- const hasCustomizableOptions = needsMcpChoice || needsSkillsChoice;
219
- if (!hasCustomizableOptions)
220
- return [];
221
- return [
222
- {
223
- id: "mode",
224
- question: "Use default settings or customize?",
225
- phase: "after_checks",
226
- options: [
227
- {
228
- value: "defaults",
229
- label: hasApp
230
- ? "Use defaults (Neon CLI, MCP: global, skills: project-level — already-configured components will be skipped)"
231
- : "Use defaults (Neon CLI, MCP: global — skills included in template)",
232
- },
233
- {
234
- value: "customize",
235
- label: "Customize installation settings",
236
- },
237
- ],
238
- default: "defaults",
239
- },
240
- ];
241
- })(),
242
- {
243
- id: "mcpScope",
244
- question: "Where should the Neon MCP server be configured?",
245
- context: "SKIP this question entirely if the mcp_server check found it is already configured. Only ask if MCP is NOT yet configured. NOTE: Cursor disables project-level MCP servers by default — if the user is in Cursor, recommend global scope or warn that they will need to manually enable the server in Cursor Settings > MCP.",
246
- phase: "after_checks",
247
- options: [
248
- {
249
- value: "global",
250
- label: "Global (available in all projects)",
251
- },
252
- {
253
- value: "project",
254
- label: "Project-level (scoped to this project only)",
255
- },
256
- {
257
- value: "none",
258
- label: "Skip — do not install the MCP server",
259
- },
260
- ],
261
- default: "global",
262
- condition: { preferenceId: "mode", equals: "customize" },
263
- group: "customize",
264
- },
265
- // Show skills scope when skills aren't detected and no partial install exists.
266
- // Partial installations are auto-completed to the same scope silently.
267
- ...(!options.skillsInstalled &&
268
- !String(options.skillsScope ?? "").includes("partial")
269
- ? [
270
- {
271
- id: "skillsScope",
272
- question: "Where should Neon agent skills be installed?",
273
- context: "Only ask if skills are not already installed.",
274
- phase: "after_checks",
275
- options: [
276
- {
277
- value: "global",
278
- label: "Global (available in all projects)",
279
- },
280
- {
281
- value: "project",
282
- label: "Project-level (scoped to this project only)",
283
- },
284
- ],
285
- default: "project",
286
- condition: {
287
- preferenceId: "mode",
288
- equals: "customize",
289
- },
290
- group: "customize",
291
- },
292
- ]
293
- : []),
294
- {
295
- id: "installExtension",
296
- question: "Install the Neon editor extension for local database browsing?",
297
- phase: "after_checks",
298
- options: [
299
- { value: "true", label: "Yes" },
300
- { value: "false", label: "No" },
301
- ],
302
- default: "true",
303
- context: "The extension installs into the IDE, NOT the agent. If the CLI detected the IDE (see detectedIde field), use that — e.g. Claude Code running inside Cursor means the IDE is Cursor and the extension IS applicable. Only applicable for VS Code-based IDEs (VS Code, Cursor, Windsurf). SKIP this question if the user is NOT in a VS Code-based IDE, or if the extension_installed check found it is already installed. Set installExtension to false in reportBack if skipped.",
304
- condition: { preferenceId: "mode", equals: "customize" },
305
- group: "customize",
306
- },
307
- ],
308
- reportBack: {
309
- type: "run_neon_init",
310
- args: [
311
- "setup",
312
- "--json",
313
- "--data",
314
- (() => {
315
- const partialScope = String(options.skillsScope ?? "").replace("-partial", "");
316
- const hasPartial = String(options.skillsScope ?? "").includes("partial");
317
- const previewFlag = options.preview
318
- ? ", preview: true"
319
- : "";
320
- const needsMcpChoice = !options.mcpConfigured;
321
- const needsSkillsChoice = !options.skillsInstalled && !hasPartial;
322
- const hasModeQuestion = needsMcpChoice || needsSkillsChoice;
323
- const modeField = hasModeQuestion
324
- ? ", mode: string"
325
- : "";
326
- const mcpField = hasModeQuestion
327
- ? ", mcpScope?: 'global'|'project'|'none'"
328
- : "";
329
- const skillsField = needsSkillsChoice
330
- ? ", skillsScope?: string"
331
- : "";
332
- const extField = hasModeQuestion
333
- ? ", installExtension?: bool"
334
- : "";
335
- const prefilledSkills = options.skillsInstalled || hasPartial
336
- ? `, skillsScope: "${options.skillsInstalled ? options.skillsScope || "project" : partialScope}"`
337
- : skillsField;
338
- return `<json: { agent: string, ide: string, mcpConfigured: bool${prefilledSkills}${previewFlag}${modeField}${mcpField}${extField}${hasApp ? ", features?: string" : ", template: string"} }>`;
339
- })(),
340
- ],
341
- },
342
- },
343
- };
70
+ const hasApp = options.hasApp !== false;
71
+ const detectedIde = detectIde();
72
+ const installedEditors = [];
73
+ if (!detectedIde) {
74
+ if (isCursorInstalled()) installedEditors.push("cursor");
75
+ if (isVSCodeInstalled()) installedEditors.push("vscode");
76
+ }
77
+ let templatePreferences = [];
78
+ if (!hasApp) {
79
+ let templates = FALLBACK_TEMPLATES;
80
+ try {
81
+ const fetched = await fetchTemplates();
82
+ if (fetched && fetched.length > 0) templates = fetched;
83
+ } catch {}
84
+ templatePreferences = buildTemplatePreference(templates);
85
+ }
86
+ return {
87
+ phase: "setup",
88
+ status: hasApp ? "pending" : "bootstrap_needed",
89
+ detectedIde: detectedIde?.toLowerCase() ?? null,
90
+ installedEditors: installedEditors.length > 0 ? installedEditors : null,
91
+ mcpConfigured: options.mcpConfigured ?? null,
92
+ mcpScope: options.mcpScope ?? null,
93
+ skillsInstalled: options.skillsInstalled ?? null,
94
+ skillsScope: options.skillsScope ?? null,
95
+ nextAction: {
96
+ type: "agent_check",
97
+ instructions: [
98
+ "IMPORTANT: Do NOT summarize this response or ask the user for consent/confirmation before starting. Proceed IMMEDIATELY with the first userPreference question.",
99
+ "",
100
+ hasApp ? "Perform the agent checks listed above (MCP server status and your agent identity), then present each userPreference question to the user ONE AT A TIME, in order. Wait for the user's answer before showing the next question. Respect the `condition` field — only show a question if its condition is met." : "No application was detected in this directory. Ask the user if they'd like to scaffold a new project from a template (the `template` preference). Present ALL template options and the 'No thanks' option — do NOT auto-select even if there is only one template. If the user selects a template, the scaffolded template includes agent skills so skills installation will be skipped. If the user chooses 'none', continue with the remaining setup preferences normally. Then perform the agent checks and present the remaining preferences ONE AT A TIME.",
101
+ "",
102
+ `The CLI has pre-detected the following from the filesystem: MCP server: ${options.mcpConfigured ? `configured (${options.mcpScope})` : "not configured"}. Agent skills: ${options.skillsInstalled ? `installed (${options.skillsScope})` : String(options.skillsScope ?? "").includes("partial") ? `partially installed (${options.skillsScope}) — missing skills will be auto-installed to the same scope` : "not installed"}. Report these findings to the user before asking preferences. Only ask about scope/options for components that are NOT already configured. Do NOT ask about skills scope if skills are partially installed — they will be completed automatically.`,
103
+ "",
104
+ "IMPORTANT (Cursor users): Cursor disables project-level MCP servers by default as a security measure. If the user is in Cursor and chooses project-level MCP scope, warn them that they will need to manually enable the Neon server in Cursor Settings > MCP after installation. Recommend global scope for Cursor to avoid this extra step.",
105
+ "",
106
+ "GROUPING: Preferences that share the same `group` field should be presented together in a single message (e.g. list all customize options at once and let the user answer them together). Preferences without a `group` must be asked individually.",
107
+ "",
108
+ detectedIde ? `The CLI has detected the IDE as: ${detectedIde.toLowerCase()}. Include this as the "ide" field in your reportBack data. IMPORTANT: The IDE and the agent are different — you may be Claude Code (agent) running inside Cursor (IDE). The extension installs into the IDE, so if the IDE is Cursor/VS Code/Windsurf, the extension IS applicable even if you are Claude Code.` : installedEditors.length > 0 ? `No IDE detected, but the following editors are installed: ${installedEditors.join(", ")}. The "installedEditors" field in this response lists them. If the user wants the extension installed, ask which editor to install it for and include that as the "ide" field in your reportBack data. If not, set "ide" to "none".` : `No IDE or supported editors detected. Set "ide" to "none" in your reportBack data.`,
109
+ "",
110
+ "After all questions are answered, call reportBack with a single --data JSON containing: agent, ide, mcpConfigured, and all preference answers. The CLI will inspect the project and merge results automatically."
111
+ ].join("\n"),
112
+ checks: [
113
+ {
114
+ id: "neonctl",
115
+ description: "The Neon CLI will be installed or updated automatically (no action needed from the agent)",
116
+ lookFor: []
117
+ },
118
+ {
119
+ id: "mcp_server",
120
+ description: "Check if the Neon MCP server is already configured in your MCP server list",
121
+ lookFor: ["An MCP server entry named 'Neon' or with URL containing 'mcp.neon.tech'"]
122
+ },
123
+ {
124
+ id: "agent_type",
125
+ description: "Identify which coding agent is running this command",
126
+ lookFor: ["Determine which agent you are: cursor, claude-code, copilot, vscode, windsurf, codex, cline, gemini-cli, goose, opencode, or antigravity", "Report your own agent identifier — this is used to configure the MCP server for the correct tool"]
127
+ },
128
+ ...detectedIde ? [{
129
+ id: "extension_installed",
130
+ description: "Check if the Neon editor extension (databricks.neon-local-connect) is already installed in the IDE (NOT the agent — e.g. if you are Claude Code running inside Cursor, check Cursor's extensions)",
131
+ lookFor: ["Run the IDE's --list-extensions command or check installed extensions for 'databricks.neon-local-connect' or 'Neon Local Connect'", "If the extension is found, set installExtension to false in your reportBack data and SKIP the installExtension question"]
132
+ }] : []
133
+ ],
134
+ userPreferences: [
135
+ ...templatePreferences,
136
+ ...hasApp ? [{
137
+ id: "features",
138
+ question: "Which Neon features would you like to enable for this project?",
139
+ phase: "after_checks",
140
+ options: [{
141
+ value: "database",
142
+ label: "Database (always included)"
143
+ }, {
144
+ value: "database,auth",
145
+ label: "Database + Neon Auth (adds authentication via Neon)"
146
+ }],
147
+ default: "database",
148
+ context: "Database connectivity is always set up. Neon Auth adds user authentication powered by Neon. More features (Functions, AI Gateway, Object Storage) will be available soon."
149
+ }] : [],
150
+ ...(() => {
151
+ const isPartialSkills = String(options.skillsScope ?? "").includes("partial");
152
+ const needsMcpChoice = !options.mcpConfigured;
153
+ const needsSkillsChoice = !options.skillsInstalled && !isPartialSkills;
154
+ if (!(needsMcpChoice || needsSkillsChoice)) return [];
155
+ return [{
156
+ id: "mode",
157
+ question: "Use default settings or customize?",
158
+ phase: "after_checks",
159
+ options: [{
160
+ value: "defaults",
161
+ label: hasApp ? "Use defaults (Neon CLI, MCP: global, skills: project-level — already-configured components will be skipped)" : "Use defaults (Neon CLI, MCP: global — skills included in template)"
162
+ }, {
163
+ value: "customize",
164
+ label: "Customize installation settings"
165
+ }],
166
+ default: "defaults"
167
+ }];
168
+ })(),
169
+ {
170
+ id: "mcpScope",
171
+ question: "Where should the Neon MCP server be configured?",
172
+ context: "SKIP this question entirely if the mcp_server check found it is already configured. Only ask if MCP is NOT yet configured. NOTE: Cursor disables project-level MCP servers by default — if the user is in Cursor, recommend global scope or warn that they will need to manually enable the server in Cursor Settings > MCP.",
173
+ phase: "after_checks",
174
+ options: [
175
+ {
176
+ value: "global",
177
+ label: "Global (available in all projects)"
178
+ },
179
+ {
180
+ value: "project",
181
+ label: "Project-level (scoped to this project only)"
182
+ },
183
+ {
184
+ value: "none",
185
+ label: "Skip — do not install the MCP server"
186
+ }
187
+ ],
188
+ default: "global",
189
+ condition: {
190
+ preferenceId: "mode",
191
+ equals: "customize"
192
+ },
193
+ group: "customize"
194
+ },
195
+ ...!options.skillsInstalled && !String(options.skillsScope ?? "").includes("partial") ? [{
196
+ id: "skillsScope",
197
+ question: "Where should Neon agent skills be installed?",
198
+ context: "Only ask if skills are not already installed.",
199
+ phase: "after_checks",
200
+ options: [{
201
+ value: "global",
202
+ label: "Global (available in all projects)"
203
+ }, {
204
+ value: "project",
205
+ label: "Project-level (scoped to this project only)"
206
+ }],
207
+ default: "project",
208
+ condition: {
209
+ preferenceId: "mode",
210
+ equals: "customize"
211
+ },
212
+ group: "customize"
213
+ }] : [],
214
+ {
215
+ id: "installExtension",
216
+ question: "Install the Neon editor extension for local database browsing?",
217
+ phase: "after_checks",
218
+ options: [{
219
+ value: "true",
220
+ label: "Yes"
221
+ }, {
222
+ value: "false",
223
+ label: "No"
224
+ }],
225
+ default: "true",
226
+ context: "The extension installs into the IDE, NOT the agent. If the CLI detected the IDE (see detectedIde field), use that — e.g. Claude Code running inside Cursor means the IDE is Cursor and the extension IS applicable. Only applicable for VS Code-based IDEs (VS Code, Cursor, Windsurf). SKIP this question if the user is NOT in a VS Code-based IDE, or if the extension_installed check found it is already installed. Set installExtension to false in reportBack if skipped.",
227
+ condition: {
228
+ preferenceId: "mode",
229
+ equals: "customize"
230
+ },
231
+ group: "customize"
232
+ }
233
+ ],
234
+ reportBack: {
235
+ type: "run_neon_init",
236
+ args: [
237
+ "setup",
238
+ "--json",
239
+ "--data",
240
+ (() => {
241
+ const partialScope = String(options.skillsScope ?? "").replace("-partial", "");
242
+ const hasPartial = String(options.skillsScope ?? "").includes("partial");
243
+ const previewFlag = options.preview ? ", preview: true" : "";
244
+ const needsMcpChoice = !options.mcpConfigured;
245
+ const needsSkillsChoice = !options.skillsInstalled && !hasPartial;
246
+ const hasModeQuestion = needsMcpChoice || needsSkillsChoice;
247
+ const modeField = hasModeQuestion ? ", mode: string" : "";
248
+ const mcpField = hasModeQuestion ? ", mcpScope?: 'global'|'project'|'none'" : "";
249
+ const skillsField = needsSkillsChoice ? ", skillsScope?: string" : "";
250
+ const extField = hasModeQuestion ? ", installExtension?: bool" : "";
251
+ return `<json: { agent: string, ide: string, mcpConfigured: bool${options.skillsInstalled || hasPartial ? `, skillsScope: "${options.skillsInstalled ? options.skillsScope || "project" : partialScope}"` : skillsField}${previewFlag}${modeField}${mcpField}${extField}${hasApp ? ", features?: string" : ", template: string"} }>`;
252
+ })()
253
+ ]
254
+ }
255
+ }
256
+ };
344
257
  }
345
258
  /**
346
- * Executes the batched installation of MCP server, skills, and extension.
347
- * Runs commands directly in the CLI process — the agent does NOT run these.
348
- * Returns results and chains to the getting-started phase.
349
- */
259
+ * Executes the batched installation of MCP server, skills, and extension.
260
+ * Runs commands directly in the CLI process — the agent does NOT run these.
261
+ * Returns results and chains to the getting-started phase.
262
+ */
350
263
  async function executeBatchedInstallation(options) {
351
- const mcpScope = options.mcpScope ?? "global";
352
- const agentId = options.agent ?? "cursor";
353
- const mcpAgentId = resolveAddMcpAgentId(agentId);
354
- const installExt = options.installExtension === true;
355
- const results = [];
356
- const isBootstrap = !!options.template;
357
- // Step 0: Bootstrap project from template if specified
358
- if (isBootstrap && options.template) {
359
- try {
360
- const templates = await fetchTemplates();
361
- const template = findTemplate(templates, options.template) ??
362
- findTemplate(FALLBACK_TEMPLATES, options.template);
363
- if (!template) {
364
- throw new Error(`Unknown template "${options.template}".`);
365
- }
366
- await scaffoldTemplate(template, ".");
367
- results.push({
368
- id: "bootstrap",
369
- description: `Scaffolded project from template "${options.template}"`,
370
- status: "success",
371
- });
372
- // Write template features to .neon under _init (ephemeral, cleaned up when init completes)
373
- if (options.templateRequires) {
374
- const neonContextPath = resolve(process.cwd(), ".neon");
375
- const context = {
376
- _init: { features: options.templateRequires },
377
- };
378
- writeFileSync(neonContextPath, `${JSON.stringify(context, null, 2)}\n`);
379
- }
380
- }
381
- catch (err) {
382
- results.push({
383
- id: "bootstrap",
384
- description: `Failed to scaffold project from template "${options.template}"`,
385
- status: "failed",
386
- error: err instanceof Error ? err.message : "Unknown error",
387
- });
388
- }
389
- }
390
- // Step 1: Ensure the Neon CLI is installed and up to date
391
- const neonctlResult = await ensureNeonctl();
392
- switch (neonctlResult.status) {
393
- case "already_current":
394
- results.push({
395
- id: "neonctl",
396
- description: `Neon CLI is up to date (v${neonctlResult.version})`,
397
- status: "success",
398
- });
399
- break;
400
- case "installed":
401
- results.push({
402
- id: "neonctl",
403
- description: `Installed Neon CLI (v${neonctlResult.version})`,
404
- status: "success",
405
- });
406
- break;
407
- case "updated":
408
- results.push({
409
- id: "neonctl",
410
- description: `Updated Neon CLI to v${neonctlResult.version}`,
411
- status: "success",
412
- });
413
- break;
414
- case "failed":
415
- results.push({
416
- id: "neonctl",
417
- description: "Failed to install Neon CLI",
418
- status: "failed",
419
- error: neonctlResult.error,
420
- });
421
- break;
422
- }
423
- // Step 2: Install MCP server (skip if already configured)
424
- const isCursor = mcpAgentId === "cursor" ||
425
- options.ide?.toLowerCase() === "cursor" ||
426
- options.agent?.toLowerCase() === "cursor";
427
- if (mcpScope === "none") {
428
- results.push({
429
- id: "skip_mcp",
430
- description: "Neon MCP server installation skipped by user",
431
- status: "success",
432
- });
433
- }
434
- else if (options.mcpConfigured) {
435
- results.push({
436
- id: "skip_mcp",
437
- description: "Neon MCP server already configured",
438
- status: "success",
439
- });
440
- }
441
- else {
442
- const mcpArgs = [
443
- "-y",
444
- "add-mcp",
445
- "https://mcp.neon.tech/mcp",
446
- ...(mcpScope === "global" ? ["-g"] : []),
447
- "-n",
448
- "Neon",
449
- "-y",
450
- "-a",
451
- mcpAgentId,
452
- ];
453
- try {
454
- await execa("npx", mcpArgs, { stdio: "pipe", timeout: 60000 });
455
- results.push({
456
- id: "install_mcp",
457
- description: `Installed Neon MCP server (${mcpScope} scope)`,
458
- status: "success",
459
- });
460
- // Some editors disable newly added MCP servers by default.
461
- // Cursor: project-level servers are always disabled initially.
462
- // Claude Code: newly added servers require user approval.
463
- const isClaudeCode = mcpAgentId === "claude-code" ||
464
- options.agent?.toLowerCase() === "claude-code";
465
- if (isCursor && mcpScope === "project") {
466
- results.push({
467
- id: "enable_mcp",
468
- description: 'Cursor disables project-level MCP servers by default. Open Cursor Settings > MCP and toggle the "Neon" server on.',
469
- status: "success",
470
- manualAction: true,
471
- });
472
- }
473
- else if (isClaudeCode) {
474
- results.push({
475
- id: "enable_mcp",
476
- description: 'Claude Code requires approval for newly added MCP servers. When prompted, approve the "Neon" MCP server to enable it. You can check MCP server status with /mcp in Claude Code.',
477
- status: "success",
478
- manualAction: true,
479
- });
480
- }
481
- }
482
- catch (err) {
483
- results.push({
484
- id: "install_mcp",
485
- description: "Failed to install Neon MCP server",
486
- status: "failed",
487
- error: err instanceof Error ? err.message : "Unknown error",
488
- });
489
- }
490
- }
491
- // Step 3: Install/update skills (skip when bootstrapping — templates bundle skills)
492
- if (isBootstrap) {
493
- results.push({
494
- id: "install_skills",
495
- description: "Neon agent skills included in template",
496
- status: "success",
497
- });
498
- }
499
- else {
500
- const skillsScope = (options.skillsScope ?? "project");
501
- const skillsOk = await ensureSkillsUpToDate(agentId, skillsScope, options.preview);
502
- if (skillsOk) {
503
- results.push({
504
- id: "install_skills",
505
- description: "Neon agent skills installed",
506
- status: "success",
507
- });
508
- }
509
- else {
510
- // Build the install commands for the agent to run directly
511
- // (sandboxed environments may block child process writes)
512
- const { getSkillList } = await import("../skills.js");
513
- const skillList = getSkillList(options.preview);
514
- const cmds = skillList.map((s) => `skills add neondatabase/agent-skills --skill ${s} --agent ${agentId}${skillsScope === "global" ? " -g" : ""} -y`);
515
- results.push({
516
- id: "install_skills",
517
- description: "Failed to install Neon agent skills automatically. Run these commands to install manually:",
518
- status: "failed",
519
- commands: cmds,
520
- });
521
- }
522
- }
523
- // Step 4: Install editor extension if requested
524
- // Use the agent-reported IDE (not agent identity) — e.g. Claude Code running in
525
- // Cursor should install the extension for Cursor, not skip it.
526
- if (installExt) {
527
- const extResult = await installExtensionForIde(options.ide ?? agentId);
528
- results.push(extResult);
529
- }
530
- // Step 5: Write selected features to .neon under _init for brownfield flows
531
- // (Bootstrap flows already wrote _init in step 0)
532
- if (!isBootstrap && options.features && options.features.length > 0) {
533
- const neonContextPath = resolve(process.cwd(), ".neon");
534
- const context = {
535
- _init: { features: options.features },
536
- };
537
- writeFileSync(neonContextPath, `${JSON.stringify(context, null, 2)}\n`);
538
- }
539
- const allSucceeded = results.every((r) => r.status === "success");
540
- // Build args to chain to the getting-started phase as a separate CLI call.
541
- // This ensures the agent gets a clean response with ONLY the getting-started
542
- // action — no competing "results" array to distract it.
543
- const gettingStartedData = {};
544
- if (options.connectionString)
545
- gettingStartedData.hasConnectionString = true;
546
- if (options.framework)
547
- gettingStartedData.framework = options.framework;
548
- if (options.orm)
549
- gettingStartedData.orm = options.orm;
550
- if (options.migrationTool)
551
- gettingStartedData.migrationTool = options.migrationTool;
552
- if (options.migrationDir)
553
- gettingStartedData.migrationDir = options.migrationDir;
554
- // Pass features so getting-started knows which phases to chain to
555
- const resolvedFeatures = options.templateRequires ?? options.features;
556
- if (resolvedFeatures && resolvedFeatures.length > 0)
557
- gettingStartedData.features = resolvedFeatures;
558
- // Bootstrap implies preview mode (new project in us-east required)
559
- if (isBootstrap)
560
- gettingStartedData.preview = true;
561
- const gettingStartedArgs = [
562
- "getting-started",
563
- "--json",
564
- "--data",
565
- JSON.stringify(gettingStartedData),
566
- ];
567
- return {
568
- phase: "setup",
569
- status: allSucceeded ? "installed" : "partial",
570
- results,
571
- nextAction: {
572
- type: "run_neon_init",
573
- args: gettingStartedArgs,
574
- },
575
- };
264
+ const mcpScope = options.mcpScope ?? "global";
265
+ const agentId = options.agent ?? "cursor";
266
+ const mcpAgentId = resolveAddMcpAgentId(agentId);
267
+ const installExt = options.installExtension === true;
268
+ const results = [];
269
+ const isBootstrap = !!options.template;
270
+ if (isBootstrap && options.template) try {
271
+ const templates = await fetchTemplates();
272
+ const template = findTemplate(templates, options.template) ?? findTemplate(FALLBACK_TEMPLATES, options.template);
273
+ if (!template) throw new Error(`Unknown template "${options.template}".`);
274
+ await scaffoldTemplate(template, ".");
275
+ results.push({
276
+ id: "bootstrap",
277
+ description: `Scaffolded project from template "${options.template}"`,
278
+ status: "success"
279
+ });
280
+ if (options.templateRequires) {
281
+ const neonContextPath = resolve(process.cwd(), ".neon");
282
+ const context = { _init: { features: options.templateRequires } };
283
+ writeFileSync(neonContextPath, `${JSON.stringify(context, null, 2)}\n`);
284
+ }
285
+ } catch (err) {
286
+ results.push({
287
+ id: "bootstrap",
288
+ description: `Failed to scaffold project from template "${options.template}"`,
289
+ status: "failed",
290
+ error: err instanceof Error ? err.message : "Unknown error"
291
+ });
292
+ }
293
+ const neonctlResult = await ensureNeonctl();
294
+ switch (neonctlResult.status) {
295
+ case "already_current":
296
+ results.push({
297
+ id: "neonctl",
298
+ description: `Neon CLI is up to date (v${neonctlResult.version})`,
299
+ status: "success"
300
+ });
301
+ break;
302
+ case "installed":
303
+ results.push({
304
+ id: "neonctl",
305
+ description: `Installed Neon CLI (v${neonctlResult.version})`,
306
+ status: "success"
307
+ });
308
+ break;
309
+ case "updated":
310
+ results.push({
311
+ id: "neonctl",
312
+ description: `Updated Neon CLI to v${neonctlResult.version}`,
313
+ status: "success"
314
+ });
315
+ break;
316
+ case "failed": results.push({
317
+ id: "neonctl",
318
+ description: "Failed to install Neon CLI",
319
+ status: "failed",
320
+ error: neonctlResult.error
321
+ });
322
+ }
323
+ const isCursor = mcpAgentId === "cursor" || options.ide?.toLowerCase() === "cursor" || options.agent?.toLowerCase() === "cursor";
324
+ if (mcpScope === "none") results.push({
325
+ id: "skip_mcp",
326
+ description: "Neon MCP server installation skipped by user",
327
+ status: "success"
328
+ });
329
+ else if (options.mcpConfigured) results.push({
330
+ id: "skip_mcp",
331
+ description: "Neon MCP server already configured",
332
+ status: "success"
333
+ });
334
+ else {
335
+ const mcpArgs = [
336
+ "-y",
337
+ "add-mcp",
338
+ "https://mcp.neon.tech/mcp",
339
+ ...mcpScope === "global" ? ["-g"] : [],
340
+ "-n",
341
+ "Neon",
342
+ "-y",
343
+ "-a",
344
+ mcpAgentId
345
+ ];
346
+ try {
347
+ await execa("npx", mcpArgs, {
348
+ stdio: "pipe",
349
+ timeout: 6e4
350
+ });
351
+ results.push({
352
+ id: "install_mcp",
353
+ description: `Installed Neon MCP server (${mcpScope} scope)`,
354
+ status: "success"
355
+ });
356
+ const isClaudeCode = mcpAgentId === "claude-code" || options.agent?.toLowerCase() === "claude-code";
357
+ if (isCursor && mcpScope === "project") results.push({
358
+ id: "enable_mcp",
359
+ description: "Cursor disables project-level MCP servers by default. Open Cursor Settings > MCP and toggle the \"Neon\" server on.",
360
+ status: "success",
361
+ manualAction: true
362
+ });
363
+ else if (isClaudeCode) results.push({
364
+ id: "enable_mcp",
365
+ description: "Claude Code requires approval for newly added MCP servers. When prompted, approve the \"Neon\" MCP server to enable it. You can check MCP server status with /mcp in Claude Code.",
366
+ status: "success",
367
+ manualAction: true
368
+ });
369
+ } catch (err) {
370
+ results.push({
371
+ id: "install_mcp",
372
+ description: "Failed to install Neon MCP server",
373
+ status: "failed",
374
+ error: err instanceof Error ? err.message : "Unknown error"
375
+ });
376
+ }
377
+ }
378
+ if (isBootstrap) results.push({
379
+ id: "install_skills",
380
+ description: "Neon agent skills included in template",
381
+ status: "success"
382
+ });
383
+ else {
384
+ const skillsScope = options.skillsScope ?? "project";
385
+ if (await ensureSkillsUpToDate(agentId, skillsScope, options.preview)) results.push({
386
+ id: "install_skills",
387
+ description: "Neon agent skills installed",
388
+ status: "success"
389
+ });
390
+ else {
391
+ const { getSkillList } = await import("../skills.js");
392
+ const cmds = getSkillList(options.preview).map((s) => `skills add neondatabase/agent-skills --skill ${s} --agent ${agentId}${skillsScope === "global" ? " -g" : ""} -y`);
393
+ results.push({
394
+ id: "install_skills",
395
+ description: "Failed to install Neon agent skills automatically. Run these commands to install manually:",
396
+ status: "failed",
397
+ commands: cmds
398
+ });
399
+ }
400
+ }
401
+ if (installExt) {
402
+ const extResult = await installExtensionForIde(options.ide ?? agentId);
403
+ results.push(extResult);
404
+ }
405
+ if (!isBootstrap && options.features && options.features.length > 0) {
406
+ const neonContextPath = resolve(process.cwd(), ".neon");
407
+ const context = { _init: { features: options.features } };
408
+ writeFileSync(neonContextPath, `${JSON.stringify(context, null, 2)}\n`);
409
+ }
410
+ const allSucceeded = results.every((r) => r.status === "success");
411
+ const gettingStartedData = {};
412
+ if (options.connectionString) gettingStartedData.hasConnectionString = true;
413
+ if (options.framework) gettingStartedData.framework = options.framework;
414
+ if (options.orm) gettingStartedData.orm = options.orm;
415
+ if (options.migrationTool) gettingStartedData.migrationTool = options.migrationTool;
416
+ if (options.migrationDir) gettingStartedData.migrationDir = options.migrationDir;
417
+ const resolvedFeatures = options.templateRequires ?? options.features;
418
+ if (resolvedFeatures && resolvedFeatures.length > 0) gettingStartedData.features = resolvedFeatures;
419
+ if (isBootstrap) gettingStartedData.preview = true;
420
+ const gettingStartedArgs = [
421
+ "getting-started",
422
+ "--json",
423
+ "--data",
424
+ JSON.stringify(gettingStartedData)
425
+ ];
426
+ return {
427
+ phase: "setup",
428
+ status: allSucceeded ? "installed" : "partial",
429
+ results,
430
+ nextAction: {
431
+ type: "run_neon_init",
432
+ args: gettingStartedArgs
433
+ }
434
+ };
576
435
  }
577
436
  /**
578
- * Fills in missing filesystem inspection fields by running inspectProject().
579
- * Agent-reported data (mcpConfigured, agent, mode, scopes) is preserved.
580
- * CLI-detectable fields (framework, orm, migrations, connectionString, isVscodeIde)
581
- * are filled in only if not already present.
582
- */
437
+ * Fills in missing filesystem inspection fields by running inspectProject().
438
+ * Agent-reported data (mcpConfigured, agent, mode, scopes) is preserved.
439
+ * CLI-detectable fields (framework, orm, migrations, connectionString, isVscodeIde)
440
+ * are filled in only if not already present.
441
+ */
583
442
  async function mergeCliInspection(options) {
584
- // If the agent already provided these, no need to re-inspect
585
- if (options.framework !== undefined && options.orm !== undefined) {
586
- return options;
587
- }
588
- const inspection = await inspectProject([
589
- { id: "connection_string", description: "", lookFor: [] },
590
- { id: "project_stack", description: "", lookFor: [] },
591
- { id: "migrations", description: "", lookFor: [] },
592
- { id: "ide_type", description: "", lookFor: [] },
593
- ]);
594
- // Also detect IDE if not already reported by the agent
595
- const ide = options.ide?.toLowerCase().replace(/\s+/g, "-") ||
596
- detectIde()?.toLowerCase().replace(/\s+/g, "-") ||
597
- undefined;
598
- return {
599
- ...options,
600
- ide,
601
- connectionString: options.connectionString ??
602
- inspection.connectionString,
603
- framework: options.framework ?? inspection.framework,
604
- orm: options.orm ?? inspection.orm,
605
- migrationTool: options.migrationTool ??
606
- inspection.migrationTool,
607
- migrationDir: options.migrationDir ??
608
- inspection.migrationDir,
609
- isVscodeIde: options.isVscodeIde ??
610
- inspection.isVscodeIde,
611
- };
443
+ if (options.framework !== void 0 && options.orm !== void 0) return options;
444
+ const inspection = await inspectProject([
445
+ {
446
+ id: "connection_string",
447
+ description: "",
448
+ lookFor: []
449
+ },
450
+ {
451
+ id: "project_stack",
452
+ description: "",
453
+ lookFor: []
454
+ },
455
+ {
456
+ id: "migrations",
457
+ description: "",
458
+ lookFor: []
459
+ },
460
+ {
461
+ id: "ide_type",
462
+ description: "",
463
+ lookFor: []
464
+ }
465
+ ]);
466
+ const ide = options.ide?.toLowerCase().replace(/\s+/g, "-") || detectIde()?.toLowerCase().replace(/\s+/g, "-") || void 0;
467
+ return {
468
+ ...options,
469
+ ide,
470
+ connectionString: options.connectionString ?? inspection.connectionString,
471
+ framework: options.framework ?? inspection.framework,
472
+ orm: options.orm ?? inspection.orm,
473
+ migrationTool: options.migrationTool ?? inspection.migrationTool,
474
+ migrationDir: options.migrationDir ?? inspection.migrationDir,
475
+ isVscodeIde: options.isVscodeIde ?? inspection.isVscodeIde
476
+ };
612
477
  }
613
478
  function isVscodeBasedIde(options) {
614
- if (options.ide) {
615
- const ide = options.ide.toLowerCase();
616
- return (ide === "cursor" ||
617
- ide === "vscode" ||
618
- ide === "vs-code" ||
619
- ide === "windsurf");
620
- }
621
- return options.isVscodeIde === true;
479
+ if (options.ide) {
480
+ const ide = options.ide.toLowerCase();
481
+ return ide === "cursor" || ide === "vscode" || ide === "vs-code" || ide === "windsurf";
482
+ }
483
+ return options.isVscodeIde === true;
622
484
  }
623
485
  /**
624
- * Resolves which IDE to install the extension for.
625
- * Accepts the agent-reported IDE value (preferred), the agent ID, or
626
- * falls back to env-var detection.
627
- */
486
+ * Resolves which IDE to install the extension for.
487
+ * Accepts the agent-reported IDE value (preferred), the agent ID, or
488
+ * falls back to env-var detection.
489
+ */
628
490
  function resolveEditorForExtension(ideOrAgentId) {
629
- // Map known IDE/agent identifiers to Editor types
630
- switch (ideOrAgentId.toLowerCase()) {
631
- case "cursor":
632
- return "Cursor";
633
- case "vscode":
634
- case "vs-code":
635
- case "copilot":
636
- case "github-copilot":
637
- case "github-copilot-cli":
638
- return "VS Code";
639
- default:
640
- break;
641
- }
642
- // Fall back to env-var detection
643
- const ide = detectIde();
644
- if (ide === "Cursor" || ide === "VS Code")
645
- return ide;
646
- return null;
491
+ switch (ideOrAgentId.toLowerCase()) {
492
+ case "cursor": return "Cursor";
493
+ case "vscode":
494
+ case "vs-code":
495
+ case "copilot":
496
+ case "github-copilot":
497
+ case "github-copilot-cli": return "VS Code";
498
+ }
499
+ const ide = detectIde();
500
+ if (ide === "Cursor" || ide === "VS Code") return ide;
501
+ return null;
647
502
  }
648
503
  const MANUAL_INSTALL_MSG = `Search for "Neon" in the extensions panel (Cmd+Shift+X / Ctrl+Shift+X) and install "Neon Local Connect" by Databricks.`;
649
504
  /**
650
- * Installs the Neon extension for the detected IDE.
651
- *
652
- * Uses env-var detection to determine the IDE (not the agent identity),
653
- * so Claude Code running in Cursor correctly installs for Cursor.
654
- *
655
- * Strategy:
656
- * 1. Try `<editor> --install-extension <id>` directly (uses editor's configured marketplace)
657
- * 2. If that fails, download .vsix (from proxy or Open VSX) and install via local file
658
- * 3. If all else fails: return manual install instructions
659
- */
505
+ * Installs the Neon extension for the detected IDE.
506
+ *
507
+ * Uses env-var detection to determine the IDE (not the agent identity),
508
+ * so Claude Code running in Cursor correctly installs for Cursor.
509
+ *
510
+ * Strategy:
511
+ * 1. Try `<editor> --install-extension <id>` directly (uses editor's configured marketplace)
512
+ * 2. If that fails, download .vsix (from proxy or Open VSX) and install via local file
513
+ * 3. If all else fails: return manual install instructions
514
+ */
660
515
  async function installExtensionForIde(agentId) {
661
- const editorType = resolveEditorForExtension(agentId);
662
- if (!editorType) {
663
- return {
664
- id: "install_extension",
665
- description: MANUAL_INSTALL_MSG,
666
- status: "success",
667
- manualAction: true,
668
- };
669
- }
670
- const editorCmd = await findEditorCommand(editorType);
671
- if (!editorCmd) {
672
- return {
673
- id: "install_extension",
674
- description: MANUAL_INSTALL_MSG,
675
- status: "success",
676
- manualAction: true,
677
- };
678
- }
679
- // Try direct marketplace install first (works if editor has marketplace configured)
680
- try {
681
- await execa(editorCmd, ["--install-extension", NEON_EXTENSION_ID], {
682
- stdio: "pipe",
683
- timeout: 60000,
684
- });
685
- return {
686
- id: "install_extension",
687
- description: `Installed Neon extension for ${editorType}`,
688
- status: "success",
689
- };
690
- }
691
- catch {
692
- // Fall through to VSIX download approach
693
- }
694
- // Download .vsix and install locally
695
- const vsixPath = await downloadVsix();
696
- if (!vsixPath) {
697
- return {
698
- id: "install_extension",
699
- description: MANUAL_INSTALL_MSG,
700
- status: "success",
701
- manualAction: true,
702
- };
703
- }
704
- try {
705
- await execa(editorCmd, ["--install-extension", vsixPath], {
706
- stdio: "pipe",
707
- timeout: 60000,
708
- });
709
- return {
710
- id: "install_extension",
711
- description: `Installed Neon extension for ${editorType}`,
712
- status: "success",
713
- };
714
- }
715
- catch {
716
- return {
717
- id: "install_extension",
718
- description: MANUAL_INSTALL_MSG,
719
- status: "success",
720
- manualAction: true,
721
- };
722
- }
723
- finally {
724
- try {
725
- await unlink(vsixPath);
726
- }
727
- catch { }
728
- }
516
+ const editorType = resolveEditorForExtension(agentId);
517
+ if (!editorType) return {
518
+ id: "install_extension",
519
+ description: MANUAL_INSTALL_MSG,
520
+ status: "success",
521
+ manualAction: true
522
+ };
523
+ const editorCmd = await findEditorCommand(editorType);
524
+ if (!editorCmd) return {
525
+ id: "install_extension",
526
+ description: MANUAL_INSTALL_MSG,
527
+ status: "success",
528
+ manualAction: true
529
+ };
530
+ try {
531
+ await execa(editorCmd, ["--install-extension", NEON_EXTENSION_ID], {
532
+ stdio: "pipe",
533
+ timeout: 6e4
534
+ });
535
+ return {
536
+ id: "install_extension",
537
+ description: `Installed Neon extension for ${editorType}`,
538
+ status: "success"
539
+ };
540
+ } catch {}
541
+ const vsixPath = await downloadVsix();
542
+ if (!vsixPath) return {
543
+ id: "install_extension",
544
+ description: MANUAL_INSTALL_MSG,
545
+ status: "success",
546
+ manualAction: true
547
+ };
548
+ try {
549
+ await execa(editorCmd, ["--install-extension", vsixPath], {
550
+ stdio: "pipe",
551
+ timeout: 6e4
552
+ });
553
+ return {
554
+ id: "install_extension",
555
+ description: `Installed Neon extension for ${editorType}`,
556
+ status: "success"
557
+ };
558
+ } catch {
559
+ return {
560
+ id: "install_extension",
561
+ description: MANUAL_INSTALL_MSG,
562
+ status: "success",
563
+ manualAction: true
564
+ };
565
+ } finally {
566
+ try {
567
+ await unlink(vsixPath);
568
+ } catch {}
569
+ }
729
570
  }
571
+ //#endregion
572
+ export { handleSetupPhase };