neon 4.6.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -113
- package/dist/_chunks/{auth_selection-ktL6uoEe.js → auth_selection-pitMFzDe.js} +1 -1
- package/dist/_chunks/{credential_io-B1JxWELN.js → credential_io-YeAxg9Xn.js} +2 -2
- package/dist/_chunks/{paths-g3DRqJiD.js → paths-DMq0Lt7a.js} +1 -1
- package/dist/_chunks/{profiles-D95AeZ5v.js → profiles-CvnFEQyd.js} +1 -1
- package/dist/analytics.js +2 -2
- package/dist/auth_context.js +1 -1
- package/dist/commands/auth.js +12 -9
- package/dist/commands/bootstrap.js +72 -43
- package/dist/commands/claim.js +1 -1
- package/dist/commands/env.js +3 -20
- package/dist/commands/init.js +80 -79
- package/dist/commands/link.js +110 -292
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/plugins.js +3 -3
- package/dist/commands/profile.js +4 -4
- package/dist/config.js +1 -1
- package/dist/credential_io.js +1 -1
- package/dist/index.js +1 -1
- package/dist/init/child.js +36 -0
- package/dist/init/plan.js +47 -0
- package/dist/init/tooling.js +43 -0
- package/dist/init/wizard.js +42 -0
- package/dist/mcp/targets.js +1 -1
- package/dist/plugins/plan.js +1 -1
- package/dist/plugins/targets.js +5 -1
- package/dist/retire_credential.js +1 -1
- package/dist/utils/cli_name.js +1 -12
- package/dist/utils/middlewares.js +1 -1
- package/dist/utils/package_manager.js +10 -5
- package/package.json +13 -14
- package/dist/init/auth.js +0 -53
- package/dist/init/build_config.js +0 -4
- package/dist/init/detect_agent.js +0 -63
- package/dist/init/editors.js +0 -19
- package/dist/init/enrich_output.js +0 -61
- package/dist/init/extension.js +0 -154
- package/dist/init/inspect.js +0 -249
- package/dist/init/interactive.js +0 -529
- package/dist/init/neonctl.js +0 -139
- package/dist/init/orchestrate.js +0 -189
- package/dist/init/phases/auth.js +0 -195
- package/dist/init/phases/cleanup.js +0 -27
- package/dist/init/phases/db.js +0 -257
- package/dist/init/phases/getting_started.js +0 -218
- package/dist/init/phases/mcp.js +0 -219
- package/dist/init/phases/migrations.js +0 -250
- package/dist/init/phases/neon_auth.js +0 -116
- package/dist/init/phases/setup.js +0 -582
- package/dist/init/phases/skills.js +0 -86
- package/dist/init/phases/status.js +0 -82
- package/dist/init/profile_cli.js +0 -56
- package/dist/init/resolve_context.js +0 -105
- package/dist/init/route_command.js +0 -93
- package/dist/init/skills.js +0 -206
- package/dist/init/types.js +0 -1
- package/dist/init/vsix.js +0 -89
package/dist/init/interactive.js
DELETED
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
import { FALLBACK_TEMPLATES, fetchTemplates, scaffoldTemplate } from "./bootstrap.js";
|
|
2
|
-
import { detectAgent, detectIde } from "./detect_agent.js";
|
|
3
|
-
import { neonInitAgentCmd } from "./profile_cli.js";
|
|
4
|
-
import { installNeonMcpServer } from "../mcp/install.js";
|
|
5
|
-
import { agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, tryResolveAddMcpAgentId } from "../mcp/agents.js";
|
|
6
|
-
import { getSkillsAgentName, mcpPickerOptions } from "./agents.js";
|
|
7
|
-
import { ensureNeonctlAuth, isAuthenticated } from "./auth.js";
|
|
8
|
-
import { installExtension, isExtensionInstalled } from "./extension.js";
|
|
9
|
-
import { inspectProject } from "./inspect.js";
|
|
10
|
-
import { ensureNeonctl } from "./neonctl.js";
|
|
11
|
-
import { ensureSkillsUpToDate, installAgentSkills, skillsInstalledForAgent } from "./skills.js";
|
|
12
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { resolve } from "node:path";
|
|
14
|
-
import which from "which";
|
|
15
|
-
import { SelectPrompt } from "@clack/core";
|
|
16
|
-
import { confirm, isCancel, log, multiselect, outro, select, spinner } from "@clack/prompts";
|
|
17
|
-
import { bold, dim, gray, italic } from "yoctocolors";
|
|
18
|
-
import picocolors from "picocolors";
|
|
19
|
-
//#region src/init/interactive.ts
|
|
20
|
-
/**
|
|
21
|
-
* Interactive v2 CLI — purpose-built guided flow for humans.
|
|
22
|
-
* Uses the same underlying install functions but with a clean clack-based UX.
|
|
23
|
-
*/
|
|
24
|
-
function wordWrap(text, width) {
|
|
25
|
-
return text.split("\n").map((line) => {
|
|
26
|
-
if (line.length <= width) return line;
|
|
27
|
-
const words = line.split(" ");
|
|
28
|
-
const lines = [];
|
|
29
|
-
let current = "";
|
|
30
|
-
for (const word of words) if (current.length + word.length + 1 > width && current.length > 0) {
|
|
31
|
-
lines.push(current);
|
|
32
|
-
current = word;
|
|
33
|
-
} else current = current.length > 0 ? `${current} ${word}` : word;
|
|
34
|
-
if (current.length > 0) lines.push(current);
|
|
35
|
-
return lines.join("\n");
|
|
36
|
-
}).join("\n");
|
|
37
|
-
}
|
|
38
|
-
const neonGreenFn = (s) => `\x1b[38;2;75;181;120m${s}\x1b[39m`;
|
|
39
|
-
const originalCyan = picocolors.cyan;
|
|
40
|
-
const originalMagenta = picocolors.magenta;
|
|
41
|
-
function patchClackColors() {
|
|
42
|
-
const pc = picocolors;
|
|
43
|
-
pc.cyan = neonGreenFn;
|
|
44
|
-
pc.magenta = neonGreenFn;
|
|
45
|
-
return () => {
|
|
46
|
-
pc.cyan = originalCyan;
|
|
47
|
-
pc.magenta = originalMagenta;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
const S_BAR = "│";
|
|
51
|
-
const S_BAR_END = "└";
|
|
52
|
-
const S_STEP_ACTIVE = "◆";
|
|
53
|
-
const S_STEP_SUBMIT = "◇";
|
|
54
|
-
const S_STEP_CANCEL = "■";
|
|
55
|
-
const S_RADIO_ACTIVE = "●";
|
|
56
|
-
const S_RADIO_INACTIVE = "○";
|
|
57
|
-
const SENTINEL_NONE = "none";
|
|
58
|
-
/**
|
|
59
|
-
* Bespoke template picker built on @clack/core's `SelectPrompt`.
|
|
60
|
-
*
|
|
61
|
-
* The stock `@clack/prompts` `select` can only render an option on a single
|
|
62
|
-
* line (title + a parenthesized hint on the focused row), which can't express
|
|
63
|
-
* what we want here: a clean title-only list, and a focused row that expands to
|
|
64
|
-
* `Title (tools)` with the description on its own italic line beneath. Driving
|
|
65
|
-
* the core prompt directly lets us own the frame, so we emit a second
|
|
66
|
-
* gutter-aligned line for the active option only.
|
|
67
|
-
*/
|
|
68
|
-
async function selectTemplate(templates, message) {
|
|
69
|
-
const options = [...templates.map((t) => ({
|
|
70
|
-
value: t.id,
|
|
71
|
-
label: t.title,
|
|
72
|
-
tools: t.tools,
|
|
73
|
-
description: t.description
|
|
74
|
-
})), {
|
|
75
|
-
value: SENTINEL_NONE,
|
|
76
|
-
label: "No thanks — continue without scaffolding"
|
|
77
|
-
}];
|
|
78
|
-
const green = neonGreenFn;
|
|
79
|
-
const descIndent = " ";
|
|
80
|
-
const descWidth = Math.max(24, (process.stdout.columns || 80) - 8);
|
|
81
|
-
const renderActive = (option) => {
|
|
82
|
-
const tools = option.tools && option.tools.length > 0 ? ` ${dim(`(${option.tools.join(", ")})`)}` : "";
|
|
83
|
-
const head = `${green(S_RADIO_ACTIVE)} ${option.label}${tools}`;
|
|
84
|
-
if (!option.description) return head;
|
|
85
|
-
return `${head}\n${wordWrap(option.description, descWidth).split("\n").map((line) => `${green(S_BAR)}${descIndent}${italic(dim(line))}`).join("\n")}`;
|
|
86
|
-
};
|
|
87
|
-
return new SelectPrompt({
|
|
88
|
-
options,
|
|
89
|
-
initialValue: options[0]?.value,
|
|
90
|
-
render() {
|
|
91
|
-
const active = this.options[this.cursor];
|
|
92
|
-
const heading = (symbol) => `${gray(S_BAR)}\n${symbol} ${message}\n`;
|
|
93
|
-
if (this.state === "submit") return `${heading(green(S_STEP_SUBMIT))}${gray(S_BAR)} ${dim(active?.label ?? "")}`;
|
|
94
|
-
if (this.state === "cancel") return `${heading(green(S_STEP_CANCEL))}${gray(S_BAR)} ${dim(active?.label ?? "")}\n${gray(S_BAR)}`;
|
|
95
|
-
const body = this.options.map((option) => option === active ? renderActive(option) : `${dim(S_RADIO_INACTIVE)} ${dim(option.label)}`).join(`\n${green(S_BAR)} `);
|
|
96
|
-
return `${heading(green(S_STEP_ACTIVE))}${green(S_BAR)} ${body}\n${green(S_BAR_END)}\n`;
|
|
97
|
-
}
|
|
98
|
-
}).prompt();
|
|
99
|
-
}
|
|
100
|
-
async function interactiveInit(options = {}) {
|
|
101
|
-
const restoreColors = patchClackColors();
|
|
102
|
-
try {
|
|
103
|
-
await interactiveInitInner(options);
|
|
104
|
-
} finally {
|
|
105
|
-
restoreColors();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async function interactiveInitInner(options) {
|
|
109
|
-
process.stdout.write(`\n\x1b[38;2;75;181;120m${[
|
|
110
|
-
" ██╗ ██╗██████╗ ██████╗ ██╗ ██╗",
|
|
111
|
-
" ███╗ ██║██╔═══╝ ██╔═══██╗███╗ ██║",
|
|
112
|
-
" ████╗██║██████╗ ██║ ██║████╗██║",
|
|
113
|
-
" ██╔████║██╔═══╝ ██║ ██║██╔████║",
|
|
114
|
-
" ██║╚███║██████╗ ╚██████╔╝██║╚███║",
|
|
115
|
-
" ╚═╝ ╚══╝╚═════╝ ╚═════╝ ╚═╝ ╚══╝"
|
|
116
|
-
].join("\n")}\x1b[0m\n`);
|
|
117
|
-
process.stdout.write(`${dim(wordWrap("\nLet's get your project set up with Neon. We'll install the MCP server, agent skills, and IDE extension, then connect your app to a database.\n", process.stdout.columns || 80))}\n`);
|
|
118
|
-
const detectedAgent = (() => {
|
|
119
|
-
const raw = detectAgent();
|
|
120
|
-
return raw ? tryResolveAddMcpAgentId(raw) : void 0;
|
|
121
|
-
})();
|
|
122
|
-
const inspectSpinner = spinner();
|
|
123
|
-
inspectSpinner.start("Checking existing configuration...");
|
|
124
|
-
const inspection = await inspectProject([
|
|
125
|
-
{
|
|
126
|
-
id: "has_app",
|
|
127
|
-
description: "",
|
|
128
|
-
lookFor: []
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: "mcp_server",
|
|
132
|
-
description: "",
|
|
133
|
-
lookFor: []
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
id: "skills",
|
|
137
|
-
description: "",
|
|
138
|
-
lookFor: []
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
id: "connection_string",
|
|
142
|
-
description: "",
|
|
143
|
-
lookFor: []
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
id: "project_stack",
|
|
147
|
-
description: "",
|
|
148
|
-
lookFor: []
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
id: "migrations",
|
|
152
|
-
description: "",
|
|
153
|
-
lookFor: []
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
id: "ide_type",
|
|
157
|
-
description: "",
|
|
158
|
-
lookFor: []
|
|
159
|
-
}
|
|
160
|
-
]);
|
|
161
|
-
inspectSpinner.stop(dim("Configuration checked ✓"));
|
|
162
|
-
const hasApp = inspection.hasApp === true;
|
|
163
|
-
let selectedFeatures = [];
|
|
164
|
-
let selectedTemplate = null;
|
|
165
|
-
if (options.preview && !hasApp) {
|
|
166
|
-
let templates = FALLBACK_TEMPLATES;
|
|
167
|
-
try {
|
|
168
|
-
const fetched = await fetchTemplates();
|
|
169
|
-
if (fetched && fetched.length > 0) templates = fetched;
|
|
170
|
-
} catch {}
|
|
171
|
-
const templateResult = await selectTemplate(templates, "No application detected. Would you like to scaffold a new project from a template?");
|
|
172
|
-
if (isCancel(templateResult)) {
|
|
173
|
-
outro("Setup cancelled.");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (templateResult !== "none") {
|
|
177
|
-
selectedTemplate = templates.find((t) => t.id === templateResult) ?? null;
|
|
178
|
-
if (selectedTemplate) {
|
|
179
|
-
selectedFeatures = selectedTemplate.requires;
|
|
180
|
-
const bootstrapS = spinner();
|
|
181
|
-
bootstrapS.start(`Scaffolding project from template "${selectedTemplate.title}"...`);
|
|
182
|
-
try {
|
|
183
|
-
await scaffoldTemplate(selectedTemplate, ".", { onWarn: (message) => log.warn(message) });
|
|
184
|
-
bootstrapS.stop(dim(`Scaffolded project from "${selectedTemplate.title}" ✓`));
|
|
185
|
-
} catch (err) {
|
|
186
|
-
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
187
|
-
bootstrapS.stop("Failed to scaffold project");
|
|
188
|
-
log.error(msg);
|
|
189
|
-
outro("Setup failed.");
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (!selectedTemplate && hasApp) {
|
|
196
|
-
const featuresResult = await select({
|
|
197
|
-
message: "Which Neon features would you like to enable for this project?",
|
|
198
|
-
options: [{
|
|
199
|
-
value: "database",
|
|
200
|
-
label: "Database"
|
|
201
|
-
}, {
|
|
202
|
-
value: "database,auth",
|
|
203
|
-
label: "Database + Neon Auth (adds authentication via Neon)"
|
|
204
|
-
}],
|
|
205
|
-
initialValue: "database"
|
|
206
|
-
});
|
|
207
|
-
if (isCancel(featuresResult)) {
|
|
208
|
-
outro("Setup cancelled.");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
selectedFeatures = featuresResult.split(",");
|
|
212
|
-
}
|
|
213
|
-
if (selectedFeatures.length > 0) {
|
|
214
|
-
const neonPath = resolve(process.cwd(), ".neon");
|
|
215
|
-
let existing = {};
|
|
216
|
-
if (existsSync(neonPath)) try {
|
|
217
|
-
existing = JSON.parse(readFileSync(neonPath, "utf-8"));
|
|
218
|
-
} catch {}
|
|
219
|
-
existing._init = { features: selectedFeatures };
|
|
220
|
-
writeFileSync(neonPath, `${JSON.stringify(existing, null, 2)}\n`);
|
|
221
|
-
}
|
|
222
|
-
const mcpHits = inspection.mcpAgents ?? [];
|
|
223
|
-
const detectedHasMcp = detectedAgent ? mcpHits.some((hit) => hit.agent === detectedAgent) : false;
|
|
224
|
-
const mcpAlready = detectedAgent ? detectedHasMcp : inspection.mcpConfigured === true;
|
|
225
|
-
const skillsAlready = selectedTemplate !== null || (detectedAgent ? skillsInstalledForAgent(detectedAgent, process.cwd(), options.preview) : inspection.skillsInstalled === true);
|
|
226
|
-
const hasNeonConnection = inspection.connectionString === true;
|
|
227
|
-
const needsInstall = !mcpAlready || !skillsAlready;
|
|
228
|
-
const neonContextPath = resolve(process.cwd(), ".neon");
|
|
229
|
-
const hasNeonContext = existsSync(neonContextPath) && (() => {
|
|
230
|
-
try {
|
|
231
|
-
return !!JSON.parse(readFileSync(neonContextPath, "utf-8")).projectId;
|
|
232
|
-
} catch {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
})();
|
|
236
|
-
const hasNeonAuth = (() => {
|
|
237
|
-
for (const envFile of [".env", ".env.local"]) {
|
|
238
|
-
const envPath = resolve(process.cwd(), envFile);
|
|
239
|
-
if (existsSync(envPath)) try {
|
|
240
|
-
const content = readFileSync(envPath, "utf-8");
|
|
241
|
-
if (/^NEON_AUTH_/m.test(content)) return true;
|
|
242
|
-
} catch {}
|
|
243
|
-
}
|
|
244
|
-
return false;
|
|
245
|
-
})();
|
|
246
|
-
let extensionAlready = false;
|
|
247
|
-
const detectedIdeEditor = extensionEditorForAgent(detectedAgent);
|
|
248
|
-
if (detectedIdeEditor) extensionAlready = await isExtensionInstalled(detectedIdeEditor);
|
|
249
|
-
if (mcpAlready && skillsAlready && hasNeonConnection && hasNeonContext) {
|
|
250
|
-
log.step(dim(`Neon MCP server already configured (${inspection.mcpScope || "detected"}) ✓`));
|
|
251
|
-
log.step(dim(`Neon agent skills already installed (${inspection.skillsScope || "detected"}) ✓`));
|
|
252
|
-
if (extensionAlready) log.step(dim("Neon editor extension installed ✓"));
|
|
253
|
-
log.step(dim("Neon database connected ✓"));
|
|
254
|
-
if (hasNeonAuth) {
|
|
255
|
-
log.step(dim("Neon Auth configured ✓"));
|
|
256
|
-
outro(dim("Your project is fully configured with Neon. Nothing to do."));
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const authResult = await select({
|
|
260
|
-
message: "Would you like to set up Neon Auth for user authentication?",
|
|
261
|
-
options: [{
|
|
262
|
-
value: "yes",
|
|
263
|
-
label: "Yes, set up Neon Auth"
|
|
264
|
-
}, {
|
|
265
|
-
value: "no",
|
|
266
|
-
label: "No, skip for now"
|
|
267
|
-
}],
|
|
268
|
-
initialValue: "no"
|
|
269
|
-
});
|
|
270
|
-
if (isCancel(authResult) || authResult === "no") {
|
|
271
|
-
outro(dim(`Your project is configured with Neon. You can set up Neon Auth later by having your agent run: ${neonInitAgentCmd({ step: "neon-auth" })}`));
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
let projectId = null;
|
|
275
|
-
try {
|
|
276
|
-
projectId = JSON.parse(readFileSync(neonContextPath, "utf-8")).projectId ?? null;
|
|
277
|
-
} catch {}
|
|
278
|
-
log.step("Next steps");
|
|
279
|
-
const promptLines = ["Set up Neon Auth for this project."];
|
|
280
|
-
if (projectId) promptLines.push(`Project ID: ${projectId}.`);
|
|
281
|
-
log.message(dim("Copy the following into your agent chat:"));
|
|
282
|
-
log.message(promptLines.map((line) => bold(neonGreenFn(line))).join("\n"));
|
|
283
|
-
outro(dim("Have feedback? Email us at feedback@neon.tech"));
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (mcpAlready) log.step(dim(`Neon MCP server already configured (${inspection.mcpScope || "detected"}) ✓`));
|
|
287
|
-
if (skillsAlready) log.step(dim(`Neon agent skills already installed (${inspection.skillsScope || "detected"}) ✓`));
|
|
288
|
-
if (needsInstall) {
|
|
289
|
-
if (!(process.env.HOME || process.env.USERPROFILE)) {
|
|
290
|
-
log.error("Could not determine home directory.");
|
|
291
|
-
outro("Setup failed.");
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
let selectedAgents;
|
|
295
|
-
if (detectedAgent) selectedAgents = [detectedAgent];
|
|
296
|
-
else {
|
|
297
|
-
const picked = await pickAgents();
|
|
298
|
-
if (!picked) {
|
|
299
|
-
outro("Setup cancelled.");
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
selectedAgents = picked;
|
|
303
|
-
}
|
|
304
|
-
let vscodeEditors = extensionEditorsFor(selectedAgents);
|
|
305
|
-
let extensionAlreadyInstalled = false;
|
|
306
|
-
if (vscodeEditors.length > 0) {
|
|
307
|
-
extensionAlreadyInstalled = (await Promise.all(vscodeEditors.map((e) => isExtensionInstalled(e)))).every(Boolean);
|
|
308
|
-
if (extensionAlreadyInstalled) log.step(dim("Neon editor extension already installed ✓"));
|
|
309
|
-
}
|
|
310
|
-
let canInstallExtension = vscodeEditors.length > 0 && !extensionAlreadyInstalled;
|
|
311
|
-
let doInstallExtension = false;
|
|
312
|
-
let mcpScope = "global";
|
|
313
|
-
let skillsScope = "project";
|
|
314
|
-
let modeResult;
|
|
315
|
-
while (true) {
|
|
316
|
-
const hintParts = [];
|
|
317
|
-
if (selectedAgents.some((agent) => !mcpHits.some((hit) => hit.agent === agent))) hintParts.push("MCP server (global)");
|
|
318
|
-
if (selectedTemplate === null && selectedAgents.some((agent) => getSkillsAgentName(agent) && !skillsInstalledForAgent(agent, process.cwd(), options.preview))) hintParts.push("agent skills (project)");
|
|
319
|
-
const agentNames = selectedAgents.map((id) => getAgentDisplayName(id)).join(", ");
|
|
320
|
-
const result = await select({
|
|
321
|
-
message: `Configure ${agentNames} for Neon:`,
|
|
322
|
-
options: [
|
|
323
|
-
{
|
|
324
|
-
value: "defaults",
|
|
325
|
-
label: "Install with defaults",
|
|
326
|
-
hint: hintParts.join(", ")
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
value: "customize",
|
|
330
|
-
label: "Customize installation",
|
|
331
|
-
hint: canInstallExtension ? "choose scopes and optional editor extension" : "choose scopes and options"
|
|
332
|
-
},
|
|
333
|
-
{
|
|
334
|
-
value: "change_editor",
|
|
335
|
-
label: "Configure a different agent"
|
|
336
|
-
}
|
|
337
|
-
],
|
|
338
|
-
initialValue: "defaults"
|
|
339
|
-
});
|
|
340
|
-
if (isCancel(result)) {
|
|
341
|
-
outro("Setup cancelled.");
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (result === "change_editor") {
|
|
345
|
-
const picked = await pickAgents();
|
|
346
|
-
if (!picked) {
|
|
347
|
-
outro("Setup cancelled.");
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
selectedAgents = picked;
|
|
351
|
-
vscodeEditors = extensionEditorsFor(selectedAgents);
|
|
352
|
-
canInstallExtension = vscodeEditors.length > 0 && !extensionAlreadyInstalled;
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
modeResult = result;
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
if (modeResult === "customize") {
|
|
359
|
-
if (selectedAgents.some((agent) => !mcpHits.some((hit) => hit.agent === agent))) {
|
|
360
|
-
const scopeResult = await select({
|
|
361
|
-
message: "Where should the Neon MCP server be configured?",
|
|
362
|
-
options: [
|
|
363
|
-
{
|
|
364
|
-
value: "global",
|
|
365
|
-
label: "Global (available in all projects)"
|
|
366
|
-
},
|
|
367
|
-
...selectedAgents.every(agentSupportsProjectMcp) ? [{
|
|
368
|
-
value: "project",
|
|
369
|
-
label: "Project-level (this project only)"
|
|
370
|
-
}] : [],
|
|
371
|
-
{
|
|
372
|
-
value: "none",
|
|
373
|
-
label: "Skip — do not install the MCP server"
|
|
374
|
-
}
|
|
375
|
-
]
|
|
376
|
-
});
|
|
377
|
-
if (isCancel(scopeResult)) {
|
|
378
|
-
outro("Setup cancelled.");
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
mcpScope = scopeResult;
|
|
382
|
-
}
|
|
383
|
-
if (selectedTemplate === null && selectedAgents.some((agent) => getSkillsAgentName(agent) && !skillsInstalledForAgent(agent, process.cwd(), options.preview))) {
|
|
384
|
-
const skillsScopeResult = await select({
|
|
385
|
-
message: "Where should Neon agent skills be installed?",
|
|
386
|
-
options: [{
|
|
387
|
-
value: "global",
|
|
388
|
-
label: "Global (available in all projects)"
|
|
389
|
-
}, {
|
|
390
|
-
value: "project",
|
|
391
|
-
label: "Project-level (this project only)"
|
|
392
|
-
}],
|
|
393
|
-
initialValue: "project"
|
|
394
|
-
});
|
|
395
|
-
if (isCancel(skillsScopeResult)) {
|
|
396
|
-
outro("Setup cancelled.");
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
skillsScope = skillsScopeResult;
|
|
400
|
-
}
|
|
401
|
-
if (canInstallExtension) {
|
|
402
|
-
const extResult = await confirm({ message: `Install the Neon extension for ${vscodeEditors.join(", ")}?` });
|
|
403
|
-
if (isCancel(extResult)) {
|
|
404
|
-
outro("Setup cancelled.");
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
doInstallExtension = extResult;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (!await isAuthenticated()) {
|
|
411
|
-
const authS = spinner();
|
|
412
|
-
authS.start("Authenticating with Neon...");
|
|
413
|
-
if (!await ensureNeonctlAuth()) {
|
|
414
|
-
authS.stop("Authentication failed.");
|
|
415
|
-
outro("Run `neon init` again after signing in.");
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
authS.stop("Authenticated.");
|
|
419
|
-
}
|
|
420
|
-
const nctlS = spinner();
|
|
421
|
-
nctlS.start("Checking Neon CLI...");
|
|
422
|
-
const nctlResult = await ensureNeonctl((phase) => {
|
|
423
|
-
nctlS.message(phase === "installing" ? "Installing Neon CLI..." : "Updating Neon CLI...");
|
|
424
|
-
});
|
|
425
|
-
switch (nctlResult.status) {
|
|
426
|
-
case "already_current":
|
|
427
|
-
nctlS.stop(dim(`Neon CLI is up to date (v${nctlResult.version}) ✓`));
|
|
428
|
-
break;
|
|
429
|
-
case "installed":
|
|
430
|
-
nctlS.stop(dim(`Installed Neon CLI (v${nctlResult.version}) ✓`));
|
|
431
|
-
break;
|
|
432
|
-
case "updated":
|
|
433
|
-
nctlS.stop(dim(`Updated Neon CLI to v${nctlResult.version} ✓`));
|
|
434
|
-
break;
|
|
435
|
-
case "failed":
|
|
436
|
-
nctlS.stop(nctlResult.action === "updating" ? "Failed to update Neon CLI" : "Failed to install Neon CLI");
|
|
437
|
-
log.warn(nctlResult.error ?? "The Neon CLI could not be installed automatically.");
|
|
438
|
-
if (which.sync("npx", { nothrow: true })) log.warn("Setup will continue using npx.");
|
|
439
|
-
}
|
|
440
|
-
for (const agent of selectedAgents) {
|
|
441
|
-
const label = getAgentDisplayName(agent);
|
|
442
|
-
if (mcpScope !== "none" && !mcpHits.some((hit) => hit.agent === agent)) {
|
|
443
|
-
const mcpS = spinner();
|
|
444
|
-
mcpS.start(`Installing Neon MCP server for ${label}...`);
|
|
445
|
-
const installed = installNeonMcpServer({
|
|
446
|
-
agent,
|
|
447
|
-
scope: mcpScope === "project" ? "project" : "global",
|
|
448
|
-
cwd: process.cwd()
|
|
449
|
-
});
|
|
450
|
-
if (installed.ok) mcpS.stop(dim(`Neon MCP server configured for ${label} (${mcpScope}) ✓`));
|
|
451
|
-
else if (installed.unsupported) {
|
|
452
|
-
mcpS.stop(`Could not write MCP config for ${label}`);
|
|
453
|
-
log.warn(installed.error);
|
|
454
|
-
} else {
|
|
455
|
-
mcpS.stop(`Failed to configure MCP server for ${label}`);
|
|
456
|
-
log.error(installed.error);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
if (selectedTemplate === null && getSkillsAgentName(agent) && !skillsInstalledForAgent(agent, process.cwd(), options.preview)) await installAgentSkills([agent], {
|
|
460
|
-
scope: skillsScope,
|
|
461
|
-
preview: options.preview
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
if (doInstallExtension) for (const editor of vscodeEditors) {
|
|
465
|
-
const extS = spinner();
|
|
466
|
-
extS.start(`Installing Neon extension for ${editor}...`);
|
|
467
|
-
if (await installExtension(editor)) extS.stop(dim(`Neon extension installed for ${editor} ✓`));
|
|
468
|
-
else extS.stop(`Extension install failed — install manually from the extensions panel.`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
const ide = detectIde();
|
|
472
|
-
const agentForSkills = detectAgent() ?? (ide === "Cursor" ? "cursor" : ide === "VS Code" ? "vscode" : ide === "Windsurf" ? "windsurf" : null);
|
|
473
|
-
if (agentForSkills) {
|
|
474
|
-
const detectedSkillsScope = inspection.skillsScope === "global" ? "global" : void 0;
|
|
475
|
-
await ensureSkillsUpToDate(agentForSkills, detectedSkillsScope, options.preview);
|
|
476
|
-
}
|
|
477
|
-
const gettingStartedData = {};
|
|
478
|
-
if (hasNeonConnection) gettingStartedData.hasConnectionString = true;
|
|
479
|
-
if (inspection.framework && inspection.framework !== "none") gettingStartedData.framework = inspection.framework;
|
|
480
|
-
if (inspection.orm && inspection.orm !== "none") gettingStartedData.orm = inspection.orm;
|
|
481
|
-
if (inspection.migrationTool && inspection.migrationTool !== "none") gettingStartedData.migrationTool = inspection.migrationTool;
|
|
482
|
-
if (inspection.migrationDir && inspection.migrationDir !== "none") gettingStartedData.migrationDir = inspection.migrationDir;
|
|
483
|
-
if (selectedFeatures.length > 0) gettingStartedData.features = selectedFeatures;
|
|
484
|
-
if (options.preview) gettingStartedData.preview = true;
|
|
485
|
-
const cmd = neonInitAgentCmd({
|
|
486
|
-
step: "getting-started",
|
|
487
|
-
...gettingStartedData
|
|
488
|
-
});
|
|
489
|
-
const cols = (process.stdout.columns || 80) - 3;
|
|
490
|
-
const promptText = `To finish setting up Neon using Neon's agent-guided onboarding experience, have your agent run this shell command: ${cmd}`;
|
|
491
|
-
log.step("Next steps");
|
|
492
|
-
log.message(dim("Copy the following into your agent chat:"));
|
|
493
|
-
log.message(wordWrap(promptText, cols).split("\n").map((line) => bold(neonGreenFn(line))).join("\n"));
|
|
494
|
-
outro(dim("Have feedback? Email us at feedback@neon.tech"));
|
|
495
|
-
}
|
|
496
|
-
function extensionEditorForAgent(agent) {
|
|
497
|
-
if (agent === "cursor") return "Cursor";
|
|
498
|
-
if (agent === "vscode") return "VS Code";
|
|
499
|
-
const ide = detectIde();
|
|
500
|
-
if (ide === "Cursor" || ide === "VS Code") return ide;
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
function extensionEditorsFor(selected) {
|
|
504
|
-
const out = [];
|
|
505
|
-
if (selected.includes("cursor")) out.push("Cursor");
|
|
506
|
-
if (selected.includes("vscode")) out.push("VS Code");
|
|
507
|
-
const ide = detectIde();
|
|
508
|
-
if ((ide === "Cursor" || ide === "VS Code") && !out.includes(ide)) out.push(ide);
|
|
509
|
-
return out;
|
|
510
|
-
}
|
|
511
|
-
async function pickAgents() {
|
|
512
|
-
const options = mcpPickerOptions();
|
|
513
|
-
const installed = await detectInstalledAgents();
|
|
514
|
-
const response = await multiselect({
|
|
515
|
-
message: "Which agent(s) would you like to configure?",
|
|
516
|
-
options,
|
|
517
|
-
initialValues: installed.filter((id) => options.some((option) => option.value === id)),
|
|
518
|
-
required: true
|
|
519
|
-
});
|
|
520
|
-
if (isCancel(response)) return null;
|
|
521
|
-
const selected = response;
|
|
522
|
-
if (selected.length === 0) {
|
|
523
|
-
log.warn("No agents selected.");
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
return selected;
|
|
527
|
-
}
|
|
528
|
-
//#endregion
|
|
529
|
-
export { interactiveInit };
|
package/dist/init/neonctl.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { globalInstallCommand, resolveInvokingPackageManager } from "../utils/package_manager.js";
|
|
2
|
-
import { explicitConfigDirCli, explicitProfileCli } from "./profile_cli.js";
|
|
3
|
-
import { lstatSync } from "node:fs";
|
|
4
|
-
import { execa } from "execa";
|
|
5
|
-
//#region src/init/neonctl.ts
|
|
6
|
-
/**
|
|
7
|
-
* `npx -y neon` avoids global-install assumptions, and `CI=` keeps emitted calls
|
|
8
|
-
* non-interactive. API and OAuth hosts stay ambient because the CLI reads them directly.
|
|
9
|
-
*/
|
|
10
|
-
function neonctlCmd() {
|
|
11
|
-
return `CI= npx -y neon${explicitProfileCli()}${explicitConfigDirCli()}`;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* The global binaries that indicate the Neon CLI is installed. The current
|
|
15
|
-
* package ships `neon`; `neonctl` is the legacy alias, kept in the probe so an
|
|
16
|
-
* older global install still counts as "installed" (and isn't reinstalled).
|
|
17
|
-
*/
|
|
18
|
-
const NEON_CLI_BINARIES = ["neon", "neonctl"];
|
|
19
|
-
/**
|
|
20
|
-
* Gets the currently installed Neon CLI version, probing the `neon` binary
|
|
21
|
-
* first and falling back to the legacy `neonctl` alias. Returns null when the
|
|
22
|
-
* CLI isn't on PATH.
|
|
23
|
-
*/
|
|
24
|
-
async function getNeonCliVersion() {
|
|
25
|
-
for (const bin of NEON_CLI_BINARIES) try {
|
|
26
|
-
const match = (await execa(bin, ["--version"], {
|
|
27
|
-
stdio: "pipe",
|
|
28
|
-
timeout: 5e3
|
|
29
|
-
})).stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
|
30
|
-
if (match) return match[1];
|
|
31
|
-
} catch {}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Checks whether the Neon CLI is globally installed and whether it's up to date.
|
|
36
|
-
*/
|
|
37
|
-
async function checkNeonctl() {
|
|
38
|
-
const currentVersion = await getNeonCliVersion();
|
|
39
|
-
if (!currentVersion) return {
|
|
40
|
-
installed: false,
|
|
41
|
-
currentVersion: null,
|
|
42
|
-
latestVersion: null,
|
|
43
|
-
needsUpdate: true
|
|
44
|
-
};
|
|
45
|
-
let latestVersion = null;
|
|
46
|
-
try {
|
|
47
|
-
latestVersion = (await execa("npm", [
|
|
48
|
-
"view",
|
|
49
|
-
"neon",
|
|
50
|
-
"version"
|
|
51
|
-
], {
|
|
52
|
-
stdio: "pipe",
|
|
53
|
-
timeout: 1e4
|
|
54
|
-
})).stdout.trim();
|
|
55
|
-
} catch {
|
|
56
|
-
return {
|
|
57
|
-
installed: true,
|
|
58
|
-
currentVersion,
|
|
59
|
-
latestVersion: null,
|
|
60
|
-
needsUpdate: false
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
const needsUpdate = currentVersion !== null && latestVersion !== null && currentVersion !== latestVersion && isOlderVersion(currentVersion, latestVersion);
|
|
64
|
-
return {
|
|
65
|
-
installed: true,
|
|
66
|
-
currentVersion,
|
|
67
|
-
latestVersion,
|
|
68
|
-
needsUpdate
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function isOlderVersion(current, latest) {
|
|
72
|
-
const c = current.split(".").map(Number);
|
|
73
|
-
const l = latest.split(".").map(Number);
|
|
74
|
-
for (let i = 0; i < 3; i++) {
|
|
75
|
-
if ((c[i] ?? 0) < (l[i] ?? 0)) return true;
|
|
76
|
-
if ((c[i] ?? 0) > (l[i] ?? 0)) return false;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Checks if neonctl is installed via a local dev symlink.
|
|
82
|
-
* If so, skip install/update — the developer manages it manually.
|
|
83
|
-
*/
|
|
84
|
-
function isLocalDevSymlink() {
|
|
85
|
-
try {
|
|
86
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
87
|
-
const roots = [`${process.env.NVM_DIR || `${home}/.nvm`}/versions/node/${process.version}/lib/node_modules`, `${home}/.nvm/versions/node/${process.version}/lib/node_modules`];
|
|
88
|
-
for (const root of roots) for (const pkg of NEON_CLI_BINARIES) try {
|
|
89
|
-
if (lstatSync(`${root}/${pkg}`).isSymbolicLink()) return true;
|
|
90
|
-
} catch {}
|
|
91
|
-
return false;
|
|
92
|
-
} catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Ensures neonctl is globally installed and up to date.
|
|
98
|
-
* Uses the same package manager that invoked the init command.
|
|
99
|
-
*/
|
|
100
|
-
async function ensureNeonctl(onProgress) {
|
|
101
|
-
if (isLocalDevSymlink()) return {
|
|
102
|
-
status: "already_current",
|
|
103
|
-
version: await getNeonCliVersion() ?? "dev"
|
|
104
|
-
};
|
|
105
|
-
const check = await checkNeonctl();
|
|
106
|
-
if (check.installed && !check.needsUpdate) return {
|
|
107
|
-
status: "already_current",
|
|
108
|
-
version: check.currentVersion ?? void 0
|
|
109
|
-
};
|
|
110
|
-
const pm = resolveInvokingPackageManager();
|
|
111
|
-
const install = globalInstallCommand(pm, "neon");
|
|
112
|
-
const action = check.installed ? "updating" : "installing";
|
|
113
|
-
if (!install) return {
|
|
114
|
-
status: "failed",
|
|
115
|
-
action,
|
|
116
|
-
error: `Could not ${action === "updating" ? "update" : "install"} the Neon CLI: this machine has no package manager that can perform a global install. npm, pnpm and bun are not on PATH (yarn Berry has no global install). Install npm, pnpm or bun, then run this again.`
|
|
117
|
-
};
|
|
118
|
-
const { command, args } = install;
|
|
119
|
-
onProgress?.(action);
|
|
120
|
-
try {
|
|
121
|
-
await execa(command, args, {
|
|
122
|
-
stdio: "pipe",
|
|
123
|
-
timeout: 6e4
|
|
124
|
-
});
|
|
125
|
-
const version = await getNeonCliVersion();
|
|
126
|
-
return {
|
|
127
|
-
status: check.installed ? "updated" : "installed",
|
|
128
|
-
version: version ?? void 0
|
|
129
|
-
};
|
|
130
|
-
} catch (err) {
|
|
131
|
-
return {
|
|
132
|
-
status: "failed",
|
|
133
|
-
action,
|
|
134
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
139
|
-
export { checkNeonctl, ensureNeonctl, neonctlCmd };
|