neon 2.44.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 (54) 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/dev/runtime.js +45 -1
  17. package/dist/dev/websocket.js +1031 -0
  18. package/dist/index.js +2 -2
  19. package/dist/init/agents.js +127 -0
  20. package/dist/init/auth.js +77 -0
  21. package/dist/init/bootstrap.js +448 -0
  22. package/dist/init/build_config.js +2 -0
  23. package/dist/init/detect_agent.js +108 -0
  24. package/dist/init/editors.js +62 -0
  25. package/dist/init/enrich_output.js +71 -0
  26. package/dist/init/extension.js +191 -0
  27. package/dist/init/inspect.js +287 -0
  28. package/dist/init/interactive.js +651 -0
  29. package/dist/init/neonctl.js +184 -0
  30. package/dist/init/orchestrate.js +190 -0
  31. package/dist/init/phases/auth.js +209 -0
  32. package/dist/init/phases/cleanup.js +27 -0
  33. package/dist/init/phases/db.js +283 -0
  34. package/dist/init/phases/getting_started.js +228 -0
  35. package/dist/init/phases/mcp.js +227 -0
  36. package/dist/init/phases/migrations.js +251 -0
  37. package/dist/init/phases/neon_auth.js +135 -0
  38. package/dist/init/phases/setup.js +729 -0
  39. package/dist/init/phases/skills.js +89 -0
  40. package/dist/init/phases/status.js +70 -0
  41. package/dist/init/resolve_context.js +107 -0
  42. package/dist/init/route_command.js +100 -0
  43. package/dist/init/skills.js +248 -0
  44. package/dist/init/types.js +1 -0
  45. package/dist/init/vsix.js +111 -0
  46. package/dist/psql/command/cmd_meta.js +2 -2
  47. package/dist/psql/core/mainloop.js +1 -1
  48. package/dist/psql/core/startup.js +1 -1
  49. package/dist/psql/core/syncVars.js +3 -3
  50. package/dist/psql/index.js +1 -1
  51. package/dist/utils/cli_name.js +14 -0
  52. package/dist/utils/esbuild.js +1 -1
  53. package/dist/utils/write_sync.js +39 -0
  54. 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
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { getRequestListener } from "@hono/node-server";
5
+ import { createUpgradeListener, installWebSocketBridge, } from "./websocket.js";
5
6
  const isFunction = (value) => typeof value === "function";
6
7
  const hasFetchMethod = (value) => typeof value === "object" &&
7
8
  value !== null &&
@@ -27,6 +28,30 @@ export const resolveFetchHandler = (mod) => {
27
28
  " export default { fetch(req) { /* ... */ } }\n" +
28
29
  " export default function (req) { /* ... */ }");
29
30
  };
31
+ const hasUpgradeMethod = (value) => typeof value === "object" &&
32
+ value !== null &&
33
+ "upgrade" in value &&
34
+ typeof value.upgrade === "function";
35
+ /**
36
+ * Resolve the user's optional WebSocket entrypoint: a named `export function upgrade`,
37
+ * or an `upgrade` method on the default export. `undefined` when the module has
38
+ * neither, which is the common case — a function without one either uses
39
+ * `upgradeWebSocket()` inside `fetch` or serves no WebSockets at all.
40
+ *
41
+ * Resolution order matches the deployed runtime exactly (named export first, then the
42
+ * default-export method), so a module that resolves one way locally cannot resolve the
43
+ * other way once deployed.
44
+ */
45
+ export const resolveUpgradeHandler = (mod) => {
46
+ if (typeof mod.upgrade === "function")
47
+ return mod.upgrade;
48
+ const defaultExport = mod.default;
49
+ if (hasUpgradeMethod(defaultExport)) {
50
+ const target = defaultExport;
51
+ return (req, socket, head) => target.upgrade(req, socket, head);
52
+ }
53
+ return undefined;
54
+ };
30
55
  /**
31
56
  * Wrap a fetch handler so user errors become a 500 response (with the message
32
57
  * in the body during dev) instead of crashing the child process.
@@ -87,11 +112,30 @@ const listen = (server, port, hostname) => new Promise((resolveListen, rejectLis
87
112
  export const startRuntime = async ({ source, port, hostname, }) => {
88
113
  const absoluteSource = resolve(process.cwd(), source);
89
114
  const mod = (await import(pathToFileURL(absoluteSource).href));
90
- const handler = withErrorBoundary(resolveFetchHandler(mod));
115
+ const fetchHandler = resolveFetchHandler(mod);
116
+ const handler = withErrorBoundary(fetchHandler);
117
+ // Publish the bridge `upgradeWebSocket()` reads before the user module can serve a
118
+ // request, so the helper resolves locally exactly as it does when deployed.
119
+ installWebSocketBridge();
91
120
  const listener = getRequestListener(handler, { hostname });
92
121
  const server = createServer((incoming, outgoing) => {
93
122
  void listener(incoming, outgoing);
94
123
  });
124
+ // Node emits 'upgrade' rather than 'request' for a WebSocket handshake. Without
125
+ // this listener Node hands the handshake to the ordinary request handler, which
126
+ // answers 200 on a connection the client expects to be a 101 — so a function's
127
+ // WebSocket code silently never ran under `neon dev`.
128
+ //
129
+ // The upgrade path takes the RAW handler, not the error-boundary-wrapped one. The
130
+ // boundary turns a throw into a 500 Response, which on this path is
131
+ // indistinguishable from a handler that deliberately declined the upgrade — so a
132
+ // crashing handler would answer 501 ("no WebSocket support") instead of surfacing
133
+ // the error. The upgrade listener has its own equivalent boundary, and reports a
134
+ // throw as the 502 the deployed runtime returns.
135
+ server.on("upgrade", createUpgradeListener({
136
+ fetch: fetchHandler,
137
+ upgrade: resolveUpgradeHandler(mod),
138
+ }));
95
139
  const boundPort = await bindPort(server, port, hostname);
96
140
  process.stdout.write(`neon-dev:ready ${boundPort}\n`);
97
141
  return boundPort;