neon 2.45.0 → 2.46.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 (52) hide show
  1. package/README.md +62 -1
  2. package/dist/_shared/paths.js +3 -4
  3. package/dist/commands/bootstrap.js +12 -11
  4. package/dist/commands/checkout.js +7 -6
  5. package/dist/commands/config.js +2 -1
  6. package/dist/commands/data_api.js +4 -3
  7. package/dist/commands/env.js +5 -4
  8. package/dist/commands/functions.js +3 -2
  9. package/dist/commands/init.js +82 -37
  10. package/dist/commands/ip_allow.js +3 -2
  11. package/dist/commands/link.js +17 -16
  12. package/dist/commands/projects.js +3 -2
  13. package/dist/commands/set_context.js +5 -4
  14. package/dist/current_branch_fast_path.js +2 -1
  15. package/dist/dev/env.js +8 -7
  16. package/dist/index.js +2 -2
  17. package/dist/init/agents.js +127 -0
  18. package/dist/init/auth.js +77 -0
  19. package/dist/init/bootstrap.js +448 -0
  20. package/dist/init/build_config.js +2 -0
  21. package/dist/init/detect_agent.js +108 -0
  22. package/dist/init/editors.js +62 -0
  23. package/dist/init/enrich_output.js +71 -0
  24. package/dist/init/extension.js +191 -0
  25. package/dist/init/inspect.js +287 -0
  26. package/dist/init/interactive.js +651 -0
  27. package/dist/init/neonctl.js +184 -0
  28. package/dist/init/orchestrate.js +190 -0
  29. package/dist/init/phases/auth.js +209 -0
  30. package/dist/init/phases/cleanup.js +27 -0
  31. package/dist/init/phases/db.js +283 -0
  32. package/dist/init/phases/getting_started.js +228 -0
  33. package/dist/init/phases/mcp.js +227 -0
  34. package/dist/init/phases/migrations.js +251 -0
  35. package/dist/init/phases/neon_auth.js +135 -0
  36. package/dist/init/phases/setup.js +729 -0
  37. package/dist/init/phases/skills.js +89 -0
  38. package/dist/init/phases/status.js +70 -0
  39. package/dist/init/resolve_context.js +107 -0
  40. package/dist/init/route_command.js +100 -0
  41. package/dist/init/skills.js +248 -0
  42. package/dist/init/types.js +1 -0
  43. package/dist/init/vsix.js +111 -0
  44. package/dist/psql/command/cmd_meta.js +2 -2
  45. package/dist/psql/core/mainloop.js +1 -1
  46. package/dist/psql/core/startup.js +1 -1
  47. package/dist/psql/core/syncVars.js +3 -3
  48. package/dist/psql/index.js +1 -1
  49. package/dist/utils/cli_name.js +14 -0
  50. package/dist/utils/esbuild.js +1 -1
  51. package/dist/utils/write_sync.js +39 -0
  52. package/package.json +18 -12
@@ -0,0 +1,729 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { unlink } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { execa } from "execa";
5
+ import { resolveAddMcpAgentId } from "../agents.js";
6
+ import { FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, } from "../bootstrap.js";
7
+ import { detectIde, isCursorInstalled, isVSCodeInstalled, } from "../detect_agent.js";
8
+ import { findEditorCommand } from "../extension.js";
9
+ import { inspectProject } from "../inspect.js";
10
+ import { ensureNeonctl } from "../neonctl.js";
11
+ import { ensureSkillsUpToDate } from "../skills.js";
12
+ import { downloadVsix, NEON_EXTENSION_ID } from "../vsix.js";
13
+ /**
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);
72
+ }
73
+ 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
+ ];
97
+ }
98
+ 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
+ };
344
+ }
345
+ /**
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
+ */
350
+ 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
+ };
576
+ }
577
+ /**
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
+ */
583
+ 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
+ };
612
+ }
613
+ 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;
622
+ }
623
+ /**
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
+ */
628
+ 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;
647
+ }
648
+ 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
+ /**
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
+ */
660
+ 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
+ }
729
+ }