diff --git a/.changeset/init-install-neon-not-neonctl.md b/.changeset/init-install-neon-not-neonctl.md new file mode 100644 index 00000000..98996be9 --- /dev/null +++ b/.changeset/init-install-neon-not-neonctl.md @@ -0,0 +1,5 @@ +--- +"neon": patch +--- + +`neon init` now detects and installs the `neon` CLI instead of the retired `neonctl` alias. Version probing checks `neon` first (falling back to `neonctl` so an existing global install still counts as installed), the update check reads `npm view neon`, and auth/context lookups shell out to `neon`. diff --git a/packages/cli/src/init/__snapshots__/agent_snapshot.test.ts.snap b/packages/cli/src/init/__snapshots__/agent_snapshot.test.ts.snap index c6059cac..692abf1d 100644 --- a/packages/cli/src/init/__snapshots__/agent_snapshot.test.ts.snap +++ b/packages/cli/src/init/__snapshots__/agent_snapshot.test.ts.snap @@ -946,8 +946,8 @@ exports[`neon init --agent: every step, against every project shape > connected --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -985,8 +985,8 @@ exports[`neon init --agent: every step, against every project shape > connected --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -1974,8 +1974,8 @@ exports[`neon init --agent: every step, against every project shape > drizzle > --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -2013,8 +2013,8 @@ exports[`neon init --agent: every step, against every project shape > drizzle > --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -2986,8 +2986,8 @@ exports[`neon init --agent: every step, against every project shape > fully-conf --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version" `; @@ -3023,8 +3023,8 @@ exports[`neon init --agent: every step, against every project shape > fully-conf --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version" `; @@ -3999,8 +3999,8 @@ exports[`neon init --agent: every step, against every project shape > greenfield --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -4038,8 +4038,8 @@ exports[`neon init --agent: every step, against every project shape > greenfield --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -5032,8 +5032,8 @@ exports[`neon init --agent: every step, against every project shape > next-prism --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -5071,8 +5071,8 @@ exports[`neon init --agent: every step, against every project shape > next-prism --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -6065,8 +6065,8 @@ exports[`neon init --agent: every step, against every project shape > next-prism --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -6104,8 +6104,8 @@ exports[`neon init --agent: every step, against every project shape > next-prism --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -7093,8 +7093,8 @@ exports[`neon init --agent: every step, against every project shape > node-app > --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -7132,8 +7132,8 @@ exports[`neon init --agent: every step, against every project shape > node-app > --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -8126,8 +8126,8 @@ exports[`neon init --agent: every step, against every project shape > other-data --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -8165,8 +8165,8 @@ exports[`neon init --agent: every step, against every project shape > other-data --- stderr --- --- subprocesses --- -/neonctl --version -/npm view neonctl version +/neon --version +/npm view neon version /npx -y add-mcp https://mcp.neon.tech/mcp -g -n Neon -y -a cursor /skills --version /skills add neondatabase/agent-skills --skill neon --agent cursor -y @@ -9570,7 +9570,7 @@ exports[`neon init --agent: the orchestrator picks the next phase > fully-config --- stderr --- --- subprocesses --- -/npx -y neonctl orgs list --output json" +/npx -y neon orgs list --output json" `; exports[`neon init --agent: the orchestrator picks the next phase > fully-configured — no --data 1`] = ` @@ -9602,7 +9602,7 @@ exports[`neon init --agent: the orchestrator picks the next phase > fully-config --- stderr --- --- subprocesses --- -/npx -y neonctl orgs list --output json +/npx -y neon orgs list --output json /skills --version" `; diff --git a/packages/cli/src/init/agent_snapshot.test.ts b/packages/cli/src/init/agent_snapshot.test.ts index 963a1d87..3b362baa 100644 --- a/packages/cli/src/init/agent_snapshot.test.ts +++ b/packages/cli/src/init/agent_snapshot.test.ts @@ -62,8 +62,14 @@ const CLI = /** Every executable the init flow shells out to, with a fixed answer. */ const STUBS: Record = { - // `neonctl --version` and `npm view neonctl version` agreeing is what makes + // `neon --version` and `npm view neon version` agreeing is what makes // `ensureNeonctl` report `already_current` instead of trying to install. + neon: `case "$1" in + --version) echo "2.45.0" ;; + *) exit 0 ;; +esac`, + // Legacy alias, still probed as a fallback by getNeonCliVersion; versioned + // so the fallback path also resolves cleanly if it's ever taken. neonctl: `case "$1" in --version) echo "2.45.0" ;; *) exit 0 ;; @@ -71,7 +77,6 @@ esac`, npm: `if [ "$1" = "view" ]; then echo "2.45.0"; fi exit 0`, npx: "exit 0", - neon: "exit 0", skills: `if [ "$1" = "--version" ]; then echo "1.0.0"; fi exit 0`, claude: `if [ "$1" = "--version" ]; then echo "1.0.0 (Claude Code)"; fi diff --git a/packages/cli/src/init/auth.ts b/packages/cli/src/init/auth.ts index 704060bb..2d4f73b6 100644 --- a/packages/cli/src/init/auth.ts +++ b/packages/cli/src/init/auth.ts @@ -12,7 +12,7 @@ export type AuthOptions = { }; /** - * Ensures neonctl is authenticated by running a command that triggers auth if needed + * Ensures the Neon CLI is authenticated by running a command that triggers auth if needed * This will automatically start the OAuth flow if the user isn't already authenticated */ export async function ensureNeonctlAuth( @@ -25,11 +25,11 @@ export async function ensureNeonctlAuth( if (existingToken) return true; try { - // Use execa to authenticate with neonctl - await execa("npx", ["-y", "neonctl", "me"], { + // Run `neon me`, which triggers the OAuth flow when not signed in. + await execa("npx", ["-y", "neon", "me"], { // Shows OAuth URL and prompts to the user stdio: "inherit", - // Unset CI so neonctl doesn't refuse to open the browser (e.g. when run from agent chat) + // Unset CI so the CLI doesn't refuse to open the browser (e.g. when run from agent chat) env: { ...process.env, CI: undefined }, }); return true; @@ -49,7 +49,7 @@ export async function ensureNeonctlAuth( } /** - * Checks whether neonctl has stored OAuth credentials. + * Checks whether the Neon CLI has stored OAuth credentials. */ export async function isAuthenticated(): Promise { const token = await getNeonctlAccessToken(); diff --git a/packages/cli/src/init/neonctl.ts b/packages/cli/src/init/neonctl.ts index 0ae98d52..830a0b0e 100644 --- a/packages/cli/src/init/neonctl.ts +++ b/packages/cli/src/init/neonctl.ts @@ -6,12 +6,14 @@ import { } from "../utils/package_manager.js"; /** - * Returns the Neon CLI command prefix: "CI= npx -y neon". + * Returns the Neon CLI command prefix emitted to users and agents: + * `CI= npx -y neon`. * * The CLI reads NEON_API_HOST and NEON_OAUTH_HOST from the environment - * directly, so no extra flags are needed. The `neon` package ships both the - * `neon` and `neonctl` binaries; we surface the cleaner `neon` command in the - * examples emitted to users and agents. + * directly, so no extra flags are needed. The published package is `neon` + * (its binary is `neon`; the older `neonctl` alias is no longer shipped). `npx` + * runs it with zero global-install assumptions, and the leading `CI=` keeps the + * CLI non-interactive. * * Usage: `${neonctlCmd()} orgs list --output json` */ @@ -27,29 +29,38 @@ type NeonctlStatus = { }; /** - * Gets the currently available neonctl version. - * Tries the global binary first, then falls back to npx. + * The global binaries that indicate the Neon CLI is installed. The current + * package ships `neon`; `neonctl` is the legacy alias, kept in the probe so an + * older global install still counts as "installed" (and isn't reinstalled). */ -async function getNeonctlVersion(): Promise { - // Try global binary first (fast path) - try { - const result = await execa("neonctl", ["--version"], { - stdio: "pipe", - timeout: 5000, - }); - const match = result.stdout.trim().match(/(\d+\.\d+\.\d+)/); - if (match) return match[1]; - } catch { - // Not globally installed — that's fine +const NEON_CLI_BINARIES = ["neon", "neonctl"] as const; + +/** + * Gets the currently installed Neon CLI version, probing the `neon` binary + * first and falling back to the legacy `neonctl` alias. Returns null when the + * CLI isn't on PATH. + */ +async function getNeonCliVersion(): Promise { + for (const bin of NEON_CLI_BINARIES) { + try { + const result = await execa(bin, ["--version"], { + stdio: "pipe", + timeout: 5000, + }); + const match = result.stdout.trim().match(/(\d+\.\d+\.\d+)/); + if (match) return match[1]; + } catch { + // This binary isn't installed — try the next one. + } } return null; } /** - * Checks whether the neonctl CLI is globally installed and whether it's up to date. + * Checks whether the Neon CLI is globally installed and whether it's up to date. */ export async function checkNeonctl(): Promise { - const currentVersion = await getNeonctlVersion(); + const currentVersion = await getNeonCliVersion(); if (!currentVersion) { return { @@ -63,7 +74,7 @@ export async function checkNeonctl(): Promise { // Check latest version from npm registry let latestVersion: string | null = null; try { - const result = await execa("npm", ["view", "neonctl", "version"], { + const result = await execa("npm", ["view", "neon", "version"], { stdio: "pipe", timeout: 10000, }); @@ -111,17 +122,20 @@ function isLocalDevSymlink(): boolean { try { const home = process.env.HOME || process.env.USERPROFILE || ""; const nvmDir = process.env.NVM_DIR || `${home}/.nvm`; - // Check common global module locations for a symlink - const candidates = [ - `${nvmDir}/versions/node/${process.version}/lib/node_modules/neonctl`, - `${home}/.nvm/versions/node/${process.version}/lib/node_modules/neonctl`, + // Check common global module locations for a symlink, under both the + // current `neon` package name and the legacy `neonctl` one. + const roots = [ + `${nvmDir}/versions/node/${process.version}/lib/node_modules`, + `${home}/.nvm/versions/node/${process.version}/lib/node_modules`, ]; - for (const candidate of candidates) { - try { - const stat = lstatSync(candidate); - if (stat.isSymbolicLink()) return true; - } catch { - // path doesn't exist + for (const root of roots) { + for (const pkg of NEON_CLI_BINARIES) { + try { + const stat = lstatSync(`${root}/${pkg}`); + if (stat.isSymbolicLink()) return true; + } catch { + // path doesn't exist + } } } return false; @@ -137,7 +151,7 @@ function isLocalDevSymlink(): boolean { export async function ensureNeonctl(): Promise { // Skip install for local dev symlinks to avoid permission errors if (isLocalDevSymlink()) { - const version = await getNeonctlVersion(); + const version = await getNeonCliVersion(); return { status: "already_current", version: version ?? "dev", @@ -154,7 +168,7 @@ export async function ensureNeonctl(): Promise { } const pm = resolveInvokingPackageManager(); - const install = globalInstallCommand(pm, "neonctl"); + const install = globalInstallCommand(pm, "neon"); if (!install) { // The next step is installing a package manager, not falling back to // npx: npx ships with npm, so it is missing in exactly this case. @@ -172,7 +186,7 @@ export async function ensureNeonctl(): Promise { await execa(command, args, { stdio: "pipe", timeout: 60000 }); // Verify installation - const version = await getNeonctlVersion(); + const version = await getNeonCliVersion(); return { status: check.installed ? "updated" : "installed", version: version ?? undefined, diff --git a/packages/cli/src/init/resolve_context.ts b/packages/cli/src/init/resolve_context.ts index 425d14b8..6ee4432e 100644 --- a/packages/cli/src/init/resolve_context.ts +++ b/packages/cli/src/init/resolve_context.ts @@ -42,7 +42,7 @@ function extractNeonHost(cwd: string): string | null { * * Strategy: * 1. Extract endpoint hostname from .env - * 2. List orgs via neonctl + * 2. List orgs via the Neon CLI * 3. For each org, list projects * 4. For each project, get connection string and compare hostnames * 5. Return the matching org/project @@ -58,7 +58,7 @@ export async function resolveNeonContext( try { const result = await execa( "npx", - ["-y", "neonctl", "orgs", "list", "--output", "json"], + ["-y", "neon", "orgs", "list", "--output", "json"], { stdio: "pipe", timeout: 30000, @@ -80,7 +80,7 @@ export async function resolveNeonContext( "npx", [ "-y", - "neonctl", + "neon", "projects", "list", "--org-id", @@ -105,7 +105,7 @@ export async function resolveNeonContext( "npx", [ "-y", - "neonctl", + "neon", "connection-string", "--project-id", project.id,