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
@@ -4,6 +4,7 @@ import { applyContext, contextBranch, readContextFile, setContext, updateContext
4
4
  import { isCi } from "../env.js";
5
5
  import { log } from "../log.js";
6
6
  import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
7
+ import { getCliName } from "../utils/cli_name.js";
7
8
  import { hasNeonConfigFile, initCmd } from "./config.js";
8
9
  import { autoPullEnvAfterPin, renderAgentPullNote } from "./env.js";
9
10
  import { REGIONS } from "./projects.js";
@@ -34,7 +35,7 @@ export const builder = (argv) => argv
34
35
  alias: "branch-id",
35
36
  describe: "Branch name or ID to pin in the context (resolved to its ID before writing). " +
36
37
  "Without it, link only resolves the org and project — pin a branch with " +
37
- "`neonctl checkout <branch>` (link never guesses a default).",
38
+ `\`${getCliName()} checkout <branch>\` (link never guesses a default).`,
38
39
  type: "string",
39
40
  },
40
41
  params: {
@@ -76,7 +77,7 @@ export const builder = (argv) => argv
76
77
  .example([
77
78
  [
78
79
  "$0 link --project-id polished-snowflake-12345678",
79
- "Link an existing project (org is inferred); pin a branch later with 'neonctl checkout'",
80
+ `Link an existing project (org is inferred); pin a branch later with '${getCliName()} checkout'`,
80
81
  ],
81
82
  [
82
83
  "$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2",
@@ -120,9 +121,9 @@ export const handler = async (props) => {
120
121
  "Missing inputs and CI environment detected (no TTY for prompts).",
121
122
  "",
122
123
  "Use one of:",
123
- " neonctl link --agent (JSON state machine for agents)",
124
- " neonctl link --project-id <project> (link to an existing project; org is inferred)",
125
- " neonctl link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)",
124
+ ` ${getCliName()} link --agent (JSON state machine for agents)`,
125
+ ` ${getCliName()} link --project-id <project> (link to an existing project; org is inferred)`,
126
+ ` ${getCliName()} link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)`,
126
127
  ].join("\n"));
127
128
  process.exit(1);
128
129
  return;
@@ -180,7 +181,7 @@ const validateInputs = (inputs) => {
180
181
  throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
181
182
  }
182
183
  if (inputs.projectName && inputs.branch) {
183
- throw new Error("Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then `neonctl checkout <branch>`.");
184
+ throw new Error(`Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then \`${getCliName()} checkout <branch>\`.`);
184
185
  }
185
186
  };
186
187
  /**
@@ -338,7 +339,7 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
338
339
  .map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`)
339
340
  .join(", ")
340
341
  : "(none)";
341
- throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`neonctl checkout <branch>\`.`, "NOT_FOUND");
342
+ throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`${getCliName()} checkout <branch>\`.`, "NOT_FOUND");
342
343
  };
343
344
  /**
344
345
  * The value to persist for a branch: prefer its human-readable **name** (nicer
@@ -717,7 +718,7 @@ const runAgent = async (props, inputs) => {
717
718
  context_file: props.contextFile,
718
719
  context: { orgId, projectId },
719
720
  project: { id: projectId },
720
- message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run \`neonctl checkout <branch>\` (omit the branch to list options) to pin one and pull its env vars.`,
721
+ message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run \`${getCliName()} checkout <branch>\` (omit the branch to list options) to pin one and pull its env vars.`,
721
722
  });
722
723
  return;
723
724
  }
@@ -737,7 +738,7 @@ const runAgent = async (props, inputs) => {
737
738
  name: region.name,
738
739
  default: region.default,
739
740
  })),
740
- next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-name ${shellArg(projectName)} --region-id <region_id>`,
741
+ next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name ${shellArg(projectName)} --region-id <region_id>`,
741
742
  });
742
743
  return;
743
744
  }
@@ -781,7 +782,7 @@ const runAgent = async (props, inputs) => {
781
782
  // the instruction rather than silently dropped.
782
783
  const projects = await listAllProjects(props, orgId);
783
784
  const branchNote = branch
784
- ? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then \`neonctl checkout ${branch}\`.`
785
+ ? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then \`${getCliName()} checkout ${branch}\`.`
785
786
  : "";
786
787
  emitAgent({
787
788
  status: "needs_project",
@@ -796,9 +797,9 @@ const runAgent = async (props, inputs) => {
796
797
  })),
797
798
  create_option: {
798
799
  instruction: "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
799
- next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`,
800
+ next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`,
800
801
  },
801
- next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
802
+ next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
802
803
  });
803
804
  };
804
805
  const emitAgent = (response) => {
@@ -865,7 +866,7 @@ const buildNeedsOrgResponse = (resolution) => {
865
866
  status: "needs_org",
866
867
  instruction: "This Neon API key is organization-scoped, so the CLI cannot list the user's organizations and no existing project was found to auto-detect the org ID. Ask the user for their Neon organization ID (visible in the Neon Console under the org's Settings page, formatted like `org-bitter-breeze-12345678`) and re-run the next_command_template with that --org-id.",
867
868
  options: [],
868
- next_command_template: "neonctl link --agent --org-id <org_id>",
869
+ next_command_template: `${getCliName()} link --agent --org-id <org_id>`,
869
870
  };
870
871
  }
871
872
  const orgs = resolution.orgs;
@@ -875,7 +876,7 @@ const buildNeedsOrgResponse = (resolution) => {
875
876
  ? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking."
876
877
  : `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ? "" : "s"} they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.`,
877
878
  options: orgs.map((org) => ({ id: org.id, name: org.name })),
878
- next_command_template: "neonctl link --agent --org-id <org_id>",
879
+ next_command_template: `${getCliName()} link --agent --org-id <org_id>`,
879
880
  };
880
881
  };
881
882
  const toAgentError = (err) => {
@@ -954,7 +955,7 @@ const resolveInteractiveBranch = async (props, projectId) => {
954
955
  const picked = await pickBranchInteractively(branches, {
955
956
  message: "Which branch would you like to link?",
956
957
  nonInteractiveMessage: "No branch could be selected without an interactive terminal. " +
957
- "Re-run `neonctl link` interactively, or `neonctl checkout <branch>` to pin one.",
958
+ `Re-run \`${getCliName()} link\` interactively, or \`${getCliName()} checkout <branch>\` to pin one.`,
958
959
  });
959
960
  if (picked.kind === "existing") {
960
961
  const existing = branches.find((b) => b.id === picked.branchId);
@@ -1031,7 +1032,7 @@ const printSummary = (_props, summary) => {
1031
1032
  }
1032
1033
  else if (summary.projectId && !summary.branch && !summary.orgOnly) {
1033
1034
  lines.push("");
1034
- lines.push("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
1035
+ lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
1035
1036
  }
1036
1037
  lines.push("");
1037
1038
  process.stdout.write(`${lines.join("\n")}\n`);
@@ -4,6 +4,7 @@ import { updateContextFile } from "../context.js";
4
4
  import { isCi } from "../env.js";
5
5
  import { log } from "../log.js";
6
6
  import { projectCreateRequest, projectUpdateRequest, } from "../parameters.gen.js";
7
+ import { getCliName } from "../utils/cli_name.js";
7
8
  import { getComputeUnits } from "../utils/compute_units.js";
8
9
  import { psql } from "../utils/psql.js";
9
10
  import { writer } from "../writer.js";
@@ -392,11 +393,11 @@ The organization ID has been saved in ${props.contextFile}
392
393
 
393
394
  If you'd like to change the default organization later, use
394
395
 
395
- neonctl link --org-id <org_id>
396
+ ${getCliName()} link --org-id <org_id>
396
397
 
397
398
  Or to clear the context file and forget the default organization
398
399
 
399
- neonctl link --clear
400
+ ${getCliName()} link --clear
400
401
 
401
402
  `);
402
403
  }
@@ -1,7 +1,8 @@
1
1
  import { applyContext } from "../context.js";
2
2
  import { log } from "../log.js";
3
+ import { getCliName } from "../utils/cli_name.js";
3
4
  export const command = "set-context";
4
- export const describe = "Deprecated: use `neonctl link`. Set the .neon context (raw write).";
5
+ export const describe = `Deprecated: use \`${getCliName()} link\`. Set the .neon context (raw write).`;
5
6
  export const builder = (argv) => argv.usage("$0 set-context [options]").options({
6
7
  "project-id": {
7
8
  describe: "Project ID",
@@ -17,9 +18,9 @@ export const builder = (argv) => argv.usage("$0 set-context [options]").options(
17
18
  },
18
19
  });
19
20
  export const handler = (props) => {
20
- log.warning("`neonctl set-context` is deprecated and will be removed in a future release. " +
21
- "Use `neonctl link` instead — it verifies inputs and infers the org for you " +
22
- "(or `neonctl link --no-checks` for the same write-without-checks behavior).");
21
+ log.warning(`\`${getCliName()} set-context\` is deprecated and will be removed in a future release. ` +
22
+ `Use \`${getCliName()} link\` instead — it verifies inputs and infers the org for you ` +
23
+ `(or \`${getCliName()} link --no-checks\` for the same write-without-checks behavior).`);
23
24
  const context = {
24
25
  projectId: props.projectId,
25
26
  orgId: props.orgId,
@@ -1,5 +1,6 @@
1
1
  import { contextBranch, currentContextFile, readContextFile, } from "./context.js";
2
2
  import { log } from "./log.js";
3
+ import { getCliName } from "./utils/cli_name.js";
3
4
  /**
4
5
  * Offline fast path for `(config) status --current-branch` (used by shell prompts).
5
6
  *
@@ -35,7 +36,7 @@ cwd = process.cwd()) => {
35
36
  process.stdout.write(`${branch}\n`);
36
37
  }
37
38
  else {
38
- log.info("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
39
+ log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
39
40
  process.exitCode = 1;
40
41
  }
41
42
  return true;
package/dist/dev/env.js CHANGED
@@ -2,6 +2,7 @@ import { loadConfigFromFile } from "@neon/config";
2
2
  import { plan, pullConfig } from "@neon/config-runtime";
3
3
  import { fetchEnvReusingSecrets, } from "@neon/env/runtime";
4
4
  import { log } from "../log.js";
5
+ import { getCliName } from "../utils/cli_name.js";
5
6
  /** The API-targeting options every runtime call forwards from the context. */
6
7
  const apiOptions = (ctx) => ({
7
8
  ...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
@@ -58,7 +59,7 @@ export const resolveNeonEnvVars = async (ctx) => {
58
59
  if (config) {
59
60
  if (!ctx.projectId || !ctx.branchId) {
60
61
  throw new MissingBranchContextError("Found a neon.ts but could not resolve the project/branch. " +
61
- "Run `neonctl link` and `neonctl checkout <branch>`, or pass " +
62
+ `Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass ` +
62
63
  "--project-id / --branch.");
63
64
  }
64
65
  // Resolve env from the policy with its `preview.functions` removed. Functions carry no
@@ -86,8 +87,8 @@ export const resolveNeonEnvVars = async (ctx) => {
86
87
  // the functions API here and only mints a storage credential when a bucket exists.
87
88
  return await fetchAndProject(pulled.config, ctx);
88
89
  }
89
- throw new MissingBranchContextError("No project/branch context found. Link a branch (`neonctl link` / " +
90
- "`neonctl checkout`) or pass --project-id and --branch.");
90
+ throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / ` +
91
+ `\`${getCliName()} checkout\`) or pass --project-id and --branch.`);
91
92
  };
92
93
  /**
93
94
  * `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
@@ -113,8 +114,8 @@ export const resolveDevEnv = async (ctx) => {
113
114
  return {
114
115
  vars: {},
115
116
  skipped: {
116
- reason: "no linked Neon branch — run `neonctl link`, then " +
117
- "`neonctl checkout <branch>`, to inject DATABASE_URL and friends",
117
+ reason: `no linked Neon branch — run \`${getCliName()} link\`, then ` +
118
+ `\`${getCliName()} checkout <branch>\`, to inject DATABASE_URL and friends`,
118
119
  },
119
120
  };
120
121
  }
@@ -167,8 +168,8 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
167
168
  const names = missing.map((change) => change.identifier).join(", ");
168
169
  throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
169
170
  "does not have it yet, so the matching env vars cannot be injected. " +
170
- "Provision it first with `neonctl deploy` (or `neonctl config apply`), " +
171
- "then re-run `neonctl dev`.");
171
+ `Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), ` +
172
+ `then re-run \`${getCliName()} dev\`.`);
172
173
  };
173
174
  /**
174
175
  * A planned change that provisions a branch-level resource the branch lacks: a
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { basename } from "node:path";
2
1
  import yargs from "yargs";
3
2
  import { hideBin } from "yargs/helpers";
4
3
  import { analyticsMiddleware, closeAnalytics, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent, } from "./analytics.js";
@@ -13,6 +12,7 @@ import { isNetworkError, matchErrorCode, NETWORK_ERROR_MESSAGE, } from "./errors
13
12
  import { showHelp } from "./help.js";
14
13
  import { log } from "./log.js";
15
14
  import pkg from "./pkg.js";
15
+ import { getCliName } from "./utils/cli_name.js";
16
16
  import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
17
17
  const NO_SUBCOMMANDS_VERBS = [
18
18
  // `api <path>` has a handler but no subcommands (like `status`), so the
@@ -159,7 +159,7 @@ builder = builder
159
159
  .group("version", "Global options:")
160
160
  .alias("version", "v")
161
161
  .completion()
162
- .scriptName(basename(process.argv[1]) === "neon" ? "neon" : "neonctl")
162
+ .scriptName(getCliName())
163
163
  .epilog("For more information, visit https://neon.com/docs/reference/neon-cli")
164
164
  .wrap(null)
165
165
  .fail(false);
@@ -0,0 +1,127 @@
1
+ /**
2
+ * All agents that can be configured via neon-init.
3
+ * Aligns with add-mcp's supported agents table.
4
+ * https://github.com/neondatabase/add-mcp#supported-agents
5
+ */
6
+ export const ALL_CONFIGURABLE_AGENTS = [
7
+ {
8
+ editor: "Cursor",
9
+ addMcpId: "cursor",
10
+ hint: "Neon Local Connect extension",
11
+ },
12
+ {
13
+ editor: "VS Code",
14
+ addMcpId: "vscode",
15
+ hint: "Neon Local Connect extension",
16
+ },
17
+ { editor: "Claude CLI", addMcpId: "claude-code", hint: "MCP Server" },
18
+ {
19
+ editor: "Claude Desktop",
20
+ addMcpId: "claude-desktop",
21
+ hint: "MCP Server",
22
+ },
23
+ { editor: "Codex", addMcpId: "codex", hint: "MCP Server" },
24
+ { editor: "OpenCode", addMcpId: "opencode", hint: "MCP Server" },
25
+ { editor: "Antigravity", addMcpId: "antigravity", hint: "MCP Server" },
26
+ { editor: "Cline", addMcpId: "cline", hint: "MCP Server" },
27
+ { editor: "Cline CLI", addMcpId: "cline-cli", hint: "MCP Server" },
28
+ { editor: "Gemini CLI", addMcpId: "gemini-cli", hint: "MCP Server" },
29
+ {
30
+ editor: "GitHub Copilot CLI",
31
+ addMcpId: "github-copilot-cli",
32
+ hint: "MCP Server",
33
+ },
34
+ { editor: "Goose", addMcpId: "goose", hint: "MCP Server" },
35
+ { editor: "MCPorter", addMcpId: "mcporter", hint: "MCP Server" },
36
+ { editor: "Zed", addMcpId: "zed", hint: "MCP Server" },
37
+ ];
38
+ export function getAddMcpAgentId(editor) {
39
+ const agent = ALL_CONFIGURABLE_AGENTS.find((a) => a.editor === editor);
40
+ if (!agent) {
41
+ throw new Error(`No add-mcp agent ID found for editor: ${editor}`);
42
+ }
43
+ return agent.addMcpId;
44
+ }
45
+ /**
46
+ * Maps a raw agent identifier (as reported by agents or passed via --agent)
47
+ * to the add-mcp compatible agent ID.
48
+ *
49
+ * This handles aliases like "copilot" → "vscode", "claude" → "claude-code", etc.
50
+ */
51
+ const AGENT_ALIAS_TO_MCP_ID = {
52
+ cursor: "cursor",
53
+ copilot: "vscode",
54
+ "github-copilot": "vscode",
55
+ "vs-code": "vscode",
56
+ vscode: "vscode",
57
+ claude: "claude-code",
58
+ "claude-code": "claude-code",
59
+ "claude-desktop": "claude-desktop",
60
+ codex: "codex",
61
+ opencode: "opencode",
62
+ antigravity: "antigravity",
63
+ cline: "cline",
64
+ "cline-cli": "cline-cli",
65
+ "gemini-cli": "gemini-cli",
66
+ gemini: "gemini-cli",
67
+ goose: "goose",
68
+ windsurf: "windsurf",
69
+ "github-copilot-cli": "github-copilot-cli",
70
+ mcporter: "mcporter",
71
+ zed: "zed",
72
+ };
73
+ export function resolveAddMcpAgentId(rawAgent) {
74
+ const resolved = AGENT_ALIAS_TO_MCP_ID[rawAgent.toLowerCase()];
75
+ if (!resolved) {
76
+ throw new Error(`Unknown agent: "${rawAgent}". Supported agents: ${Object.keys(AGENT_ALIAS_TO_MCP_ID).join(", ")}`);
77
+ }
78
+ return resolved;
79
+ }
80
+ /**
81
+ * Maps a raw agent identifier to the skills CLI agent name.
82
+ */
83
+ export function getSkillsAgentName(agent) {
84
+ switch (agent.toLowerCase()) {
85
+ case "cursor":
86
+ return "cursor";
87
+ case "copilot":
88
+ case "vscode":
89
+ case "vs-code":
90
+ case "github-copilot":
91
+ return "github-copilot";
92
+ case "claude":
93
+ case "claude-code":
94
+ return "claude-code";
95
+ case "codex":
96
+ return "codex";
97
+ case "opencode":
98
+ return "opencode";
99
+ case "antigravity":
100
+ return "antigravity";
101
+ case "cline":
102
+ return "cline";
103
+ case "gemini-cli":
104
+ return "gemini-cli";
105
+ case "goose":
106
+ return "goose";
107
+ case "claude-desktop":
108
+ return "claude-code";
109
+ case "cline-cli":
110
+ return "cline";
111
+ case "gemini":
112
+ return "gemini-cli";
113
+ case "windsurf":
114
+ return "windsurf";
115
+ case "github-copilot-cli":
116
+ return "github-copilot";
117
+ case "mcporter":
118
+ return "mcporter";
119
+ case "zed":
120
+ return "zed";
121
+ default:
122
+ // Fall back to "cursor" as a safe default — skills CLI uses the
123
+ // agent name to pick the output directory (.agents/skills, .cursor/skills, etc.)
124
+ // and "cursor" uses .agents/skills which works for all agents.
125
+ return "cursor";
126
+ }
127
+ }
@@ -0,0 +1,77 @@
1
+ import { log } from "@clack/prompts";
2
+ import { execa } from "execa";
3
+ import { inspectCredentials, interpretCredentials, } from "../_shared/credentials.js";
4
+ import { resolveConfigFile } from "../_shared/paths.js";
5
+ import { DEFAULT_PROFILE } from "../_shared/profiles.js";
6
+ /**
7
+ * Ensures neonctl is authenticated by running a command that triggers auth if needed
8
+ * This will automatically start the OAuth flow if the user isn't already authenticated
9
+ */
10
+ export async function ensureNeonctlAuth(options) {
11
+ const quiet = options?.json === true;
12
+ // If already authenticated (e.g. ran in a terminal before), we can proceed
13
+ const existingToken = await getNeonctlAccessToken();
14
+ if (existingToken)
15
+ return true;
16
+ try {
17
+ // Use execa to authenticate with neonctl
18
+ await execa("npx", ["-y", "neonctl", "me"], {
19
+ // Shows OAuth URL and prompts to the user
20
+ stdio: "inherit",
21
+ // Unset CI so neonctl doesn't refuse to open the browser (e.g. when run from agent chat)
22
+ env: { ...process.env, CI: undefined },
23
+ });
24
+ return true;
25
+ }
26
+ catch (error) {
27
+ const msg = error instanceof Error ? error.message : "Unknown error";
28
+ if (!quiet) {
29
+ if (msg.includes("interactive auth") || msg.includes("CI")) {
30
+ log.error("Auth requires an interactive terminal. Run neon init in your system terminal (outside the chat) to sign in.");
31
+ }
32
+ else {
33
+ log.error(`Authentication failed: ${msg}`);
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Checks whether neonctl has stored OAuth credentials.
41
+ */
42
+ export async function isAuthenticated() {
43
+ const token = await getNeonctlAccessToken();
44
+ return token !== null;
45
+ }
46
+ /**
47
+ * The credential the Neon CLI has stored, or `null` when there is none.
48
+ *
49
+ * Shares the reader with `neon` and `@neon/env` rather than keeping a copy — the inline path
50
+ * resolution this replaced was the third implementation of "where is the config directory", and
51
+ * it also looked only for `access_token`, so an account signed in with an API key read as not
52
+ * authenticated and got sent to a browser. See `shared/cli-core/README.md`.
53
+ *
54
+ * `null` means *absent*, and only absent. A file that exists and cannot be read throws: this
55
+ * value decides whether to start a browser sign-in, and a sign-in overwrites the file it could
56
+ * not read — as a different account, if a different one is chosen. Catching every error here
57
+ * made a damaged credential indistinguishable from a fresh machine.
58
+ */
59
+ async function getNeonctlAccessToken() {
60
+ const { path } = resolveConfigFile("credentials.json");
61
+ const read = inspectCredentials(path);
62
+ if (read.kind === "absent")
63
+ return null;
64
+ if (read.kind === "unusable") {
65
+ throw new Error(`${read.reason}. Replace it deliberately with \`neon profile create ${DEFAULT_PROFILE} --force\`, or delete the file.`);
66
+ }
67
+ // `neon init` has no profile selection — it reads the default credential and refuses
68
+ // when one is named, so `DEFAULT` is the only profile this can ever be about.
69
+ const credential = interpretCredentials(read.credentials, {
70
+ path,
71
+ profile: DEFAULT_PROFILE,
72
+ });
73
+ if (credential.kind === "api_key")
74
+ return credential.apiKey;
75
+ const token = read.credentials.access_token;
76
+ return typeof token === "string" && token.trim() !== "" ? token : null;
77
+ }